mod commands;
mod content_trust;
mod db;
mod extensions;
mod handoff;
mod logging;
mod mcp;
mod memory;
mod output;
mod paths;
mod profile;
mod recovery;
mod repo;
mod runtime_api;
mod session_boundary;
mod shipped_skills;
mod state;
mod telemetry;
mod timestamps;
use std::io::Read as _;
use std::path::PathBuf;
use std::process::ExitCode;
use anyhow::{Context as _, Result};
use clap::{Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
use output::{CommandReport, OutputFormat};
#[derive(Parser)]
#[command(
name = "ccd",
version,
about = "Host-neutral continuity and governance kernel for AI coding"
)]
struct Cli {
#[arg(long, global = true, value_enum, default_value_t = OutputFormat::Text)]
output: OutputFormat,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Describe,
#[command(alias = "init")]
Attach(AttachArgs),
Scaffold(ScaffoldArgs),
Repo {
#[command(subcommand)]
command: RepoCommand,
},
Link(LinkArgs),
Check(CheckArgs),
Preflight(PreflightArgs),
Start(StartArgs),
Status(StatusArgs),
ContextCheck(ContextCheckArgs),
PolicyCheck(PolicyCheckArgs),
Gc(GcArgs),
Unlink(UnlinkArgs),
Sync(SyncArgs),
Doctor(DoctorArgs),
Drift(DriftArgs),
Handoff {
#[command(subcommand)]
command: HandoffCommand,
},
Remember(RememberArgs),
Memory {
#[command(subcommand)]
command: MemoryCommand,
},
RuntimeState {
#[command(subcommand)]
command: RuntimeStateCommand,
},
EscalationState {
#[command(subcommand)]
command: EscalationStateCommand,
},
RadarState(RadarStateArgs),
Checkpoint(CheckpointArgs),
Recovery {
#[command(subcommand)]
command: RecoveryCommand,
},
Session {
#[command(subcommand)]
command: SessionCommand,
},
SessionState {
#[command(subcommand)]
command: SessionStateCommand,
},
Hooks {
#[command(subcommand)]
command: HooksCommand,
},
Skills {
#[command(subcommand)]
command: SkillsCommand,
},
Pod {
#[command(subcommand)]
command: PodCommand,
},
#[command(hide = true)]
McpServe,
#[cfg(feature = "daemon")]
Daemon(DaemonArgs),
}
impl Command {
fn name(&self) -> &'static str {
match self {
Self::Describe => "Describe",
Self::Attach(_) => "Attach",
Self::Scaffold(_) => "Scaffold",
Self::Repo { .. } => "Repo",
Self::Link(_) => "Link",
Self::Check(_) => "Check",
Self::Preflight(_) => "Preflight",
Self::Start(_) => "Start",
Self::Status(_) => "Status",
Self::ContextCheck(_) => "ContextCheck",
Self::PolicyCheck(_) => "PolicyCheck",
Self::Gc(_) => "Gc",
Self::Unlink(_) => "Unlink",
Self::Sync(_) => "Sync",
Self::Doctor(_) => "Doctor",
Self::Drift(_) => "Drift",
Self::Handoff { .. } => "Handoff",
Self::Remember(_) => "Remember",
Self::Memory { .. } => "Memory",
Self::RuntimeState { .. } => "RuntimeState",
Self::EscalationState { .. } => "EscalationState",
Self::RadarState(_) => "RadarState",
Self::Checkpoint(_) => "Checkpoint",
Self::Recovery { .. } => "Recovery",
Self::Session { .. } => "Session",
Self::SessionState { .. } => "SessionState",
Self::Hooks { .. } => "Hooks",
Self::Skills { .. } => "Skills",
Self::Pod { .. } => "Pod",
Self::McpServe => "McpServe",
#[cfg(feature = "daemon")]
Self::Daemon(_) => "Daemon",
}
}
}
#[derive(Subcommand)]
enum HooksCommand {
Install(HooksInstallArgs),
Check(HooksCheckArgs),
}
#[derive(Subcommand)]
enum RepoCommand {
Status(RepoStatusArgs),
List(RepoListArgs),
Relink(RepoRelinkArgs),
Merge(RepoMergeArgs),
Split(RepoSplitArgs),
}
#[derive(Subcommand)]
enum HandoffCommand {
Refresh(HandoffRefreshArgs),
Export(HandoffExportArgs),
Write(HandoffWriteArgs),
}
#[derive(Subcommand)]
enum MemoryCommand {
Search(MemorySearchArgs),
Describe(MemoryDescribeArgs),
Expand(MemoryExpandArgs),
Candidate {
#[command(subcommand)]
command: MemoryCandidateCommand,
},
Compact(MemoryCompactArgs),
Promote(MemoryPromoteArgs),
}
#[derive(Subcommand)]
enum MemoryCandidateCommand {
Admit(MemoryCandidateAdmitArgs),
}
#[derive(Subcommand)]
enum SkillsCommand {
Install(SkillsInstallArgs),
}
#[derive(Subcommand)]
enum PodCommand {
Init(PodInitArgs),
List,
Status(PodStatusArgs),
MigrateDefaults(PodMigrateDefaultsArgs),
}
#[cfg(feature = "daemon")]
#[derive(Args)]
struct DaemonArgs {
#[arg(long, default_value = "127.0.0.1")]
host: String,
#[arg(long, default_value_t = 8820)]
port: u16,
#[arg(long)]
profile: Option<String>,
#[arg(long)]
cors: bool,
}
#[derive(Args)]
struct PodInitArgs {
name: String,
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long)]
force: bool,
}
#[derive(Args)]
struct PodStatusArgs {
name: Option<String>,
#[arg(long)]
path: Option<PathBuf>,
#[arg(long)]
profile: Option<String>,
}
#[derive(Args)]
struct PodMigrateDefaultsArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long = "memory-item")]
memory_items: Vec<String>,
#[arg(long = "policy-item")]
policy_items: Vec<String>,
#[arg(long)]
adopt_suggested: bool,
#[arg(long)]
write: bool,
}
#[derive(Subcommand)]
enum RuntimeStateCommand {
Export(RuntimeStateExportArgs),
ChildBootstrap(RuntimeStateChildBootstrapArgs),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum PolicyActionFamilyArg {
#[value(name = "read_only")]
ReadOnly,
#[value(name = "local_execution")]
LocalExecution,
#[value(name = "local_isolation")]
LocalIsolation,
#[value(name = "local_finalize")]
LocalFinalize,
#[value(name = "publish_external")]
PublishExternal,
#[value(name = "shared_queue_mutation")]
SharedQueueMutation,
#[value(name = "durable_memory_mutation")]
DurableMemoryMutation,
#[value(name = "repair_destructive")]
RepairDestructive,
#[value(name = "protected_surface_mutation")]
ProtectedSurfaceMutation,
}
impl PolicyActionFamilyArg {
fn into_policy_action_family(self) -> state::policy_projection::PolicyActionFamily {
match self {
Self::ReadOnly => state::policy_projection::PolicyActionFamily::ReadOnly,
Self::LocalExecution => state::policy_projection::PolicyActionFamily::LocalExecution,
Self::LocalIsolation => state::policy_projection::PolicyActionFamily::LocalIsolation,
Self::LocalFinalize => state::policy_projection::PolicyActionFamily::LocalFinalize,
Self::PublishExternal => state::policy_projection::PolicyActionFamily::PublishExternal,
Self::SharedQueueMutation => {
state::policy_projection::PolicyActionFamily::SharedQueueMutation
}
Self::DurableMemoryMutation => {
state::policy_projection::PolicyActionFamily::DurableMemoryMutation
}
Self::RepairDestructive => {
state::policy_projection::PolicyActionFamily::RepairDestructive
}
Self::ProtectedSurfaceMutation => {
state::policy_projection::PolicyActionFamily::ProtectedSurfaceMutation
}
}
}
}
#[derive(Subcommand)]
enum RecoveryCommand {
Write(RecoveryWriteArgs),
}
#[derive(Subcommand)]
enum EscalationStateCommand {
Set(EscalationStateSetArgs),
Clear(EscalationStateClearArgs),
List(EscalationStateListArgs),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum ProjectionTargetArg {
Default,
Planning,
Session,
}
impl ProjectionTargetArg {
fn into_projection_target(self) -> state::compiled::ProjectionTarget {
match self {
Self::Default => state::compiled::ProjectionTarget::Default,
Self::Planning => state::compiled::ProjectionTarget::Planning,
Self::Session => state::compiled::ProjectionTarget::Session,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum ProjectionFormatArg {
Narrative,
Symbolic,
Bundle,
}
impl ProjectionFormatArg {
fn into_projection_format(self) -> state::compiled::ProjectionFormat {
match self {
Self::Narrative => state::compiled::ProjectionFormat::Narrative,
Self::Symbolic => state::compiled::ProjectionFormat::Symbolic,
Self::Bundle => state::compiled::ProjectionFormat::Bundle,
}
}
}
#[derive(Subcommand)]
enum SessionStateCommand {
Start(SessionStateStartArgs),
Heartbeat(SessionStateHeartbeatArgs),
Clear(SessionStateClearArgs),
Takeover(SessionStateTakeoverArgs),
Gates {
#[command(subcommand)]
command: SessionGateCommand,
},
}
#[derive(Subcommand)]
enum SessionGateCommand {
List(SessionStateArgs),
Replace(SessionGateReplaceArgs),
Seed(SessionGateSeedArgs),
SetStatus(SessionGateSetStatusArgs),
Advance(SessionGateMutateArgs),
Clear(SessionGateMutateArgs),
}
#[derive(Subcommand)]
enum SessionCommand {
Open(SessionOpenArgs),
}
#[derive(Args)]
struct SessionStateArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
}
#[derive(Args, Clone, Default)]
struct ProtectedWriteArgs {
#[arg(long = "actor-id")]
actor_id: Option<String>,
#[arg(long = "session-id")]
session_id: Option<String>,
#[arg(long = "expected-revision")]
expected_revision: Option<u64>,
}
impl ProtectedWriteArgs {
fn into_state_options(self) -> state::protected_write::ExclusiveWriteOptions {
state::protected_write::ExclusiveWriteOptions {
actor_id: self.actor_id,
session_id: self.session_id,
expected_revision: self.expected_revision,
}
}
}
#[derive(Args)]
struct SessionStateStartArgs {
#[command(flatten)]
base: SessionStateArgs,
#[arg(long, value_enum)]
mode: Option<SessionModeArg>,
#[arg(long, value_enum, default_value_t = SessionLifecycleArg::Interactive)]
lifecycle: SessionLifecycleArg,
#[arg(long = "owner-kind", value_enum)]
owner_kind: Option<AutonomousOwnerKindArg>,
#[arg(long = "actor-id")]
actor_id: Option<String>,
#[arg(long = "supervisor-id")]
supervisor_id: Option<String>,
#[arg(long = "lease-seconds")]
lease_seconds: Option<u64>,
}
#[derive(Args)]
struct SessionStateHeartbeatArgs {
#[command(flatten)]
base: SessionStateArgs,
#[arg(long = "actor-id")]
actor_id: String,
#[arg(long)]
activity: Option<String>,
}
#[derive(Args)]
struct SessionStateClearArgs {
#[command(flatten)]
base: SessionStateArgs,
#[arg(long = "actor-id")]
actor_id: Option<String>,
#[arg(long)]
reason: Option<String>,
}
#[derive(Args)]
struct SessionStateTakeoverArgs {
#[command(flatten)]
base: SessionStateArgs,
#[arg(long = "actor-id")]
actor_id: String,
#[arg(long = "supervisor-id")]
supervisor_id: Option<String>,
#[arg(long)]
reason: String,
}
#[derive(Args)]
struct SessionGateReplaceArgs {
#[command(flatten)]
base: SessionStateArgs,
#[command(flatten)]
protected_write: ProtectedWriteArgs,
#[arg(long = "gate", required = true)]
gates: Vec<String>,
}
#[derive(Args)]
struct SessionGateSeedArgs {
#[command(flatten)]
base: SessionStateArgs,
#[command(flatten)]
protected_write: ProtectedWriteArgs,
#[arg(long, value_enum)]
from: GateSeedSourceArg,
}
#[derive(Args)]
struct SessionGateSetStatusArgs {
#[command(flatten)]
base: SessionStateArgs,
#[command(flatten)]
protected_write: ProtectedWriteArgs,
#[arg(long)]
index: usize,
#[arg(long, value_enum)]
status: GateStatusArg,
}
#[derive(Args)]
struct SessionGateMutateArgs {
#[command(flatten)]
base: SessionStateArgs,
#[command(flatten)]
protected_write: ProtectedWriteArgs,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum GateSeedSourceArg {
ImmediateActions,
DefinitionOfDone,
}
impl GateSeedSourceArg {
fn into_state_source(self) -> state::session_gates::GateSeedSource {
match self {
Self::ImmediateActions => state::session_gates::GateSeedSource::ImmediateActions,
Self::DefinitionOfDone => state::session_gates::GateSeedSource::DefinitionOfDone,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum GateStatusArg {
Open,
Done,
Blocked,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum SessionModeArg {
General,
Research,
Implement,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum SessionLifecycleArg {
Interactive,
Autonomous,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum AutonomousOwnerKindArg {
RuntimeWorker,
RuntimeSupervisor,
}
impl SessionModeArg {
fn into_state_mode(self) -> state::session::SessionMode {
match self {
Self::General => state::session::SessionMode::General,
Self::Research => state::session::SessionMode::Research,
Self::Implement => state::session::SessionMode::Implement,
}
}
}
impl SessionLifecycleArg {
fn into_state_lifecycle(self) -> state::session::SessionLifecycle {
match self {
Self::Interactive => state::session::SessionLifecycle::Interactive,
Self::Autonomous => state::session::SessionLifecycle::Autonomous,
}
}
}
impl AutonomousOwnerKindArg {
fn into_state_owner_kind(self) -> state::session::SessionOwnerKind {
match self {
Self::RuntimeWorker => state::session::SessionOwnerKind::RuntimeWorker,
Self::RuntimeSupervisor => state::session::SessionOwnerKind::RuntimeSupervisor,
}
}
}
impl GateStatusArg {
fn into_status(self) -> state::session_gates::ExecutionGateStatus {
match self {
Self::Open => state::session_gates::ExecutionGateStatus::Open,
Self::Done => state::session_gates::ExecutionGateStatus::Done,
Self::Blocked => state::session_gates::ExecutionGateStatus::Blocked,
}
}
}
#[derive(Args)]
struct SessionOpenArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long)]
worktree: Option<PathBuf>,
#[arg(long)]
branch: Option<String>,
#[arg(long = "from")]
from_ref: Option<String>,
#[arg(long)]
pod: Option<String>,
}
#[derive(Args)]
struct SkillsInstallArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long, default_value_t = true)]
global: bool,
#[arg(long)]
local: bool,
}
#[derive(Args)]
struct RuntimeStateExportArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long, value_enum)]
projection_target: Option<ProjectionTargetArg>,
#[arg(long, value_enum)]
projection_format: Option<ProjectionFormatArg>,
}
#[derive(Args)]
struct RuntimeStateChildBootstrapArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
}
#[derive(Args, Clone)]
struct EscalationStateArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
}
#[derive(Args)]
struct EscalationStateSetArgs {
#[command(flatten)]
base: EscalationStateArgs,
#[command(flatten)]
protected_write: ProtectedWriteArgs,
#[arg(long, value_enum, default_value_t = EscalationKindArg::Blocking)]
kind: EscalationKindArg,
#[arg(long)]
id: Option<String>,
#[arg(long)]
reason: String,
}
#[derive(Args)]
struct EscalationStateClearArgs {
#[command(flatten)]
base: EscalationStateArgs,
#[command(flatten)]
protected_write: ProtectedWriteArgs,
#[arg(long)]
id: Option<String>,
}
#[derive(Args)]
struct EscalationStateListArgs {
#[command(flatten)]
base: EscalationStateArgs,
}
#[derive(Args)]
struct HooksInstallArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
force: bool,
}
#[derive(Args)]
struct HooksCheckArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
}
#[derive(Args)]
struct HandoffRefreshArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long)]
write: bool,
}
#[derive(Args)]
struct HandoffExportArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
}
#[derive(Args)]
struct HandoffWriteArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[command(flatten)]
protected_write: ProtectedWriteArgs,
}
#[derive(Args)]
struct RecoveryWriteArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[command(flatten)]
protected_write: ProtectedWriteArgs,
}
#[derive(Args)]
struct RememberArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long, value_enum)]
scope: Option<RememberScopeArg>,
#[arg(long = "type", value_enum, default_value_t = RememberTypeArg::Observation)]
entry_type: RememberTypeArg,
#[arg(long, value_enum, default_value_t = RememberOriginArg::Manual)]
origin: RememberOriginArg,
#[arg(long)]
source_ref: Option<String>,
#[arg(long)]
dry_run: bool,
content: String,
}
#[derive(Args)]
struct AttachArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long = "project-id")]
project_id: Option<String>,
#[arg(long)]
display_name: Option<String>,
}
#[derive(Args)]
struct ScaffoldArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
force: bool,
}
#[derive(Args)]
struct LinkArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long = "project-id")]
project_id: Option<String>,
#[arg(long)]
display_name: Option<String>,
}
#[derive(Args)]
struct RepoStatusArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
}
#[derive(Args)]
struct RepoListArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
}
#[derive(Args)]
struct RepoRelinkArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
locality_id: String,
}
#[derive(Args)]
struct RepoMergeArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
target_locality_id: String,
source_locality_id: String,
#[arg(long)]
force: bool,
}
#[derive(Args)]
struct RepoSplitArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
source_locality_id: String,
}
#[derive(Args)]
struct CheckArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
}
#[derive(Args)]
struct PreflightArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
}
#[derive(Args)]
struct StartArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long)]
refresh: bool,
#[arg(long)]
check: bool,
#[arg(long, value_delimiter = ',')]
fields: Option<Vec<String>>,
#[arg(long, value_enum)]
memory_depth: Option<output::MemoryDepth>,
#[arg(long)]
activate: bool,
#[arg(long, value_enum)]
mode: Option<SessionModeArg>,
}
#[derive(Args)]
struct StatusArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long, value_delimiter = ',')]
fields: Option<Vec<String>>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum ContextCheckTriggerArg {
Interval,
Manual,
#[value(name = "pre-compaction")]
PreCompaction,
#[value(name = "idle-reset")]
IdleReset,
Resume,
#[value(name = "supervisor-poll")]
SupervisorPoll,
}
impl ContextCheckTriggerArg {
fn into_trigger(self) -> state::radar::ContextCheckTrigger {
match self {
Self::Interval => state::radar::ContextCheckTrigger::Interval,
Self::Manual => state::radar::ContextCheckTrigger::Manual,
Self::PreCompaction => state::radar::ContextCheckTrigger::PreCompaction,
Self::IdleReset => state::radar::ContextCheckTrigger::IdleReset,
Self::Resume => state::radar::ContextCheckTrigger::Resume,
Self::SupervisorPoll => state::radar::ContextCheckTrigger::SupervisorPoll,
}
}
}
#[derive(Args)]
struct ContextCheckArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long, value_enum, default_value = "interval")]
trigger: ContextCheckTriggerArg,
#[arg(long, value_delimiter = ',')]
fields: Option<Vec<String>>,
#[arg(long, value_enum)]
memory_depth: Option<output::MemoryDepth>,
}
#[derive(Args)]
struct PolicyCheckArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long, value_enum)]
action_family: PolicyActionFamilyArg,
}
#[derive(Args)]
struct GcArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
}
#[derive(Args)]
struct UnlinkArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
}
#[derive(Args)]
struct SyncArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long)]
check: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum DoctorSeverityFilter {
Pass,
Warn,
Fail,
}
#[derive(Args)]
struct DoctorArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long, hide = true)]
skip_repo_native_checks: bool,
#[arg(long, value_enum)]
severity: Option<DoctorSeverityFilter>,
#[arg(long, value_delimiter = ',')]
fields: Option<Vec<String>>,
}
#[derive(Args)]
struct DriftArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
}
#[derive(Args)]
struct MemoryPromoteArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[command(flatten)]
protected_write: ProtectedWriteArgs,
#[arg(long)]
entry: String,
#[arg(long, value_enum)]
source_outcome: Option<MemoryPromoteSourceOutcomeArg>,
#[arg(long, value_enum, default_value = "profile-memory")]
destination: MemoryPromoteDestinationArg,
#[arg(long)]
target_file: Option<PathBuf>,
#[arg(long)]
write: bool,
}
#[derive(Args)]
struct MemoryCandidateAdmitArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[command(flatten)]
protected_write: ProtectedWriteArgs,
#[arg(long)]
entry: String,
#[arg(long, value_enum)]
source_scope: MemoryCandidateSourceScopeArg,
#[arg(long, value_enum)]
destination: MemoryCandidateDestinationArg,
#[arg(long)]
write: bool,
}
#[derive(Args)]
struct MemoryCompactArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[command(flatten)]
protected_write: ProtectedWriteArgs,
#[arg(long, value_enum)]
scope: MemoryCompactScopeArg,
#[arg(long)]
entry: Option<String>,
#[arg(long)]
keep: Option<String>,
#[arg(long, value_enum)]
decay_class: Option<MemoryDecayClassArg>,
#[arg(long, value_enum)]
review: Option<MemoryCompactReviewArg>,
#[arg(long)]
remove: bool,
#[arg(long)]
write: bool,
}
#[derive(Args)]
struct MemorySearchArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long)]
query: String,
#[arg(long, default_value_t = 5, value_parser = parse_positive_usize)]
limit: usize,
}
#[derive(Args)]
struct MemoryDescribeArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long = "native-id")]
native_id: String,
}
#[derive(Args)]
struct MemoryExpandArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long = "native-id")]
native_id: String,
#[arg(long, default_value_t = 3, value_parser = parse_positive_usize)]
limit: usize,
}
fn parse_positive_usize(raw: &str) -> Result<usize, String> {
let value = raw
.parse::<usize>()
.map_err(|_| format!("invalid value `{raw}`: expected a positive integer"))?;
if value == 0 {
return Err("expected a positive integer greater than zero".to_owned());
}
Ok(value)
}
#[derive(Args)]
struct RadarStateArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long, value_delimiter = ',')]
fields: Option<Vec<String>>,
#[arg(long, value_enum)]
memory_depth: Option<output::MemoryDepth>,
#[arg(long)]
commit: bool,
#[arg(long)]
since_session: Option<String>,
}
#[derive(Args)]
struct CheckpointArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long, value_delimiter = ',')]
fields: Option<Vec<String>>,
#[arg(long, value_enum)]
memory_depth: Option<output::MemoryDepth>,
#[arg(long)]
commit: bool,
#[arg(long)]
since_session: Option<String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
enum MemoryPromoteSourceOutcomeArg {
Active,
Superseded,
LinkOnly,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
enum MemoryPromoteDestinationArg {
#[value(
name = "work-stream-memory",
alias = "branch-memory",
alias = "work_stream-memory",
alias = "work_stream_memory"
)]
BranchMemory,
#[value(name = "pod-memory")]
PodMemory,
#[value(name = "project-memory", alias = "repo-memory")]
RepoMemory,
#[value(name = "profile-memory")]
ProfileMemory,
#[value(name = "project-truth")]
ProjectTruth,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
enum MemoryCandidateSourceScopeArg {
#[value(name = "workspace-memory", alias = "clone-memory")]
Clone,
#[value(
name = "work-stream-memory",
alias = "branch-memory",
alias = "work-stream-memory",
alias = "work_stream-memory",
alias = "work_stream_memory"
)]
Branch,
#[value(name = "pod-memory")]
Pod,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
enum MemoryCandidateDestinationArg {
#[value(
name = "work-stream-memory",
alias = "branch-memory",
alias = "work-stream-memory",
alias = "work_stream-memory",
alias = "work_stream_memory"
)]
Branch,
#[value(name = "project-memory", alias = "repo-memory")]
Repo,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
enum MemoryCompactScopeArg {
Profile,
#[value(name = "project", alias = "repo")]
Repo,
#[value(name = "work-stream", alias = "branch", alias = "work_stream")]
Branch,
#[value(name = "workspace", alias = "clone")]
Clone,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
enum MemoryCompactReviewArg {
Expired,
Superseded,
PromotionCandidate,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
enum MemoryDecayClassArg {
Permanent,
Stable,
Active,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum EscalationKindArg {
Blocking,
#[value(alias = "non_blocking")]
NonBlocking,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
enum RememberScopeArg {
Profile,
#[value(name = "project", alias = "repo")]
Repo,
#[value(name = "work-stream", alias = "branch", alias = "work_stream")]
Branch,
#[value(name = "workspace", alias = "clone")]
Clone,
Pod,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
enum RememberTypeArg {
Rule,
Constraint,
Heuristic,
Observation,
Attempt,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
enum RememberOriginArg {
Manual,
Agent,
Radar,
}
impl MemoryCompactScopeArg {
fn into_scope(self) -> memory::compact::CompactScope {
match self {
Self::Profile => memory::compact::CompactScope::Profile,
Self::Repo => memory::compact::CompactScope::Repo,
Self::Branch => memory::compact::CompactScope::Branch,
Self::Clone => memory::compact::CompactScope::Clone,
}
}
}
impl MemoryCompactReviewArg {
fn into_review_kind(self) -> memory::compact::CompactReviewKind {
match self {
Self::Expired => memory::compact::CompactReviewKind::Expired,
Self::Superseded => memory::compact::CompactReviewKind::Superseded,
Self::PromotionCandidate => memory::compact::CompactReviewKind::PromotionCandidate,
}
}
}
impl MemoryDecayClassArg {
fn into_decay_class(self) -> memory::compact::DecayClass {
match self {
Self::Permanent => memory::compact::DecayClass::Permanent,
Self::Stable => memory::compact::DecayClass::Stable,
Self::Active => memory::compact::DecayClass::Active,
}
}
}
impl EscalationKindArg {
fn into_kind(self) -> state::escalation::EscalationKind {
match self {
Self::Blocking => state::escalation::EscalationKind::Blocking,
Self::NonBlocking => state::escalation::EscalationKind::NonBlocking,
}
}
}
impl RememberScopeArg {
fn into_scope(self) -> memory::remember::RememberScope {
match self {
Self::Profile => memory::remember::RememberScope::Profile,
Self::Repo => memory::remember::RememberScope::Repo,
Self::Branch => memory::remember::RememberScope::Branch,
Self::Clone => memory::remember::RememberScope::Clone,
Self::Pod => memory::remember::RememberScope::Pod,
}
}
}
impl RememberTypeArg {
fn as_str(self) -> &'static str {
match self {
Self::Rule => "rule",
Self::Constraint => "constraint",
Self::Heuristic => "heuristic",
Self::Observation => "observation",
Self::Attempt => "attempt",
}
}
}
impl RememberOriginArg {
fn as_str(self) -> &'static str {
match self {
Self::Manual => "manual",
Self::Agent => "agent",
Self::Radar => "radar",
}
}
}
impl MemoryPromoteSourceOutcomeArg {
fn into_source_outcome(self) -> memory::promote::SourceOutcome {
match self {
Self::Active => memory::promote::SourceOutcome::Active,
Self::Superseded => memory::promote::SourceOutcome::Superseded,
Self::LinkOnly => memory::promote::SourceOutcome::LinkOnly,
}
}
}
impl MemoryCandidateSourceScopeArg {
fn into_source_scope(self) -> memory::candidate::CandidateSourceScope {
match self {
Self::Clone => memory::candidate::CandidateSourceScope::Clone,
Self::Branch => memory::candidate::CandidateSourceScope::Branch,
Self::Pod => memory::candidate::CandidateSourceScope::Pod,
}
}
}
impl MemoryCandidateDestinationArg {
fn into_destination(self) -> memory::candidate::CandidateDestination {
match self {
Self::Branch => memory::candidate::CandidateDestination::Branch,
Self::Repo => memory::candidate::CandidateDestination::Repo,
}
}
}
fn apply_since_session_delta(
mut value: serde_json::Value,
repo_path: &std::path::Path,
explicit_profile: Option<&str>,
since: &str,
current_digests: &state::compiled::ProjectionDigests,
) -> anyhow::Result<serde_json::Value> {
let session_layout = paths::state::StateLayout::resolve(
repo_path,
profile::resolve(explicit_profile)?,
)?;
let target_session_id = if since == "current" {
state::session::load_session_id(&session_layout)?
.ok_or_else(|| anyhow::anyhow!(
"--since-session current: no active session found"
))?
} else {
since.to_owned()
};
match state::projection_metadata::load_baseline_for_session(
&session_layout,
&target_session_id,
)? {
Some(obs) if obs.projection_digests.is_some() => {
let baseline = obs.projection_digests.unwrap();
value = state::radar::apply_delta_filter(value, &baseline, current_digests);
}
_ => {
if let Some(obj) = value.as_object_mut() {
obj.insert(
"delta_warning".to_owned(),
serde_json::json!(
"no baseline found for session; emitting full output"
),
);
}
}
}
Ok(value)
}
fn main() -> ExitCode {
logging::init();
let matches = cli_command().get_matches();
let output = matches
.get_one::<OutputFormat>("output")
.copied()
.unwrap_or(OutputFormat::Text);
match run_matches(&matches) {
Ok(code) => code,
Err(error) => {
output::render_error(output, &error);
ExitCode::from(1)
}
}
}
pub(crate) fn cli_command() -> clap::Command {
extensions::augment_clap(Cli::command())
}
fn run_matches(matches: &clap::ArgMatches) -> Result<ExitCode> {
let output = matches
.get_one::<OutputFormat>("output")
.copied()
.unwrap_or(OutputFormat::Text);
if let Some((subcommand_name, submatches)) = matches.subcommand() {
if let Some(result) = extensions::dispatch_cli(subcommand_name, submatches, output) {
return result;
}
}
let cli = Cli::from_arg_matches(matches)?;
run(cli)
}
fn run(cli: Cli) -> Result<ExitCode> {
let command_name = cli.command.name();
let _span = tracing::info_span!("command", name = command_name).entered();
tracing::info!(command = command_name, "dispatching command");
match cli.command {
Command::Describe => {
let report = commands::describe::run();
output::render_report(cli.output, &report)
}
Command::Attach(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::attach::run(
&repo_path,
args.profile.as_deref(),
args.project_id.as_deref(),
args.display_name.as_deref(),
)?;
output::render_report(cli.output, &report)
}
Command::Scaffold(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::scaffold::run(&repo_path, args.force)?;
output::render_report(cli.output, &report)
}
Command::Repo { command: sub } => match sub {
RepoCommand::Status(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::repo::status(&repo_path, args.profile.as_deref())?;
output::render_report(cli.output, &report)
}
RepoCommand::List(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::repo::list(&repo_path, args.profile.as_deref())?;
output::render_report(cli.output, &report)
}
RepoCommand::Relink(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report =
commands::repo::relink(&repo_path, args.profile.as_deref(), &args.locality_id)?;
output::render_report(cli.output, &report)
}
RepoCommand::Merge(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::repo::merge(
&repo_path,
args.profile.as_deref(),
&args.target_locality_id,
&args.source_locality_id,
args.force,
)?;
output::render_report(cli.output, &report)
}
RepoCommand::Split(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::repo::split(
&repo_path,
args.profile.as_deref(),
&args.source_locality_id,
)?;
output::render_report(cli.output, &report)
}
},
Command::Link(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::link::run(
&repo_path,
args.profile.as_deref(),
args.project_id.as_deref(),
args.display_name.as_deref(),
)?;
output::render_report(cli.output, &report)
}
Command::Check(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::check::run(&repo_path)?;
output::render_report(cli.output, &report)
}
Command::Preflight(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::preflight::run(&repo_path, args.profile.as_deref())?;
output::render_report(cli.output, &report)
}
Command::Start(args) => {
if args.check && args.fields.is_some() {
anyhow::bail!(
"--fields is not supported with `start --check`; the check report already includes all readiness data without field filtering"
);
}
if args.check && args.memory_depth.is_some() {
anyhow::bail!(
"--memory-depth is not supported with `start --check`"
);
}
if args.check && args.activate {
anyhow::bail!("--activate is not supported with `start --check`");
}
if args.fields.is_some() && cli.output != OutputFormat::Json {
anyhow::bail!("--fields requires --output json");
}
if args.memory_depth.is_some() && cli.output != OutputFormat::Json {
anyhow::bail!("--memory-depth requires --output json");
}
if let Some(fields) = &args.fields {
output::validate_start_fields(fields)?;
}
let repo_path = paths::cli::resolve(&args.path)?;
if args.check {
let report =
commands::start::run_check(&repo_path, args.profile.as_deref(), args.refresh)?;
output::render_report(cli.output, &report)
} else {
let use_metadata = args.memory_depth == Some(output::MemoryDepth::Metadata);
let mut report = if let Some(fields) = &args.fields {
if !output::needs_source_rendering(fields) {
commands::start::run_compiled_only(
&repo_path,
args.profile.as_deref(),
args.refresh,
)?
} else {
commands::start::run(
&repo_path,
args.profile.as_deref(),
args.refresh,
)?
}
} else {
commands::start::run(&repo_path, args.profile.as_deref(), args.refresh)?
};
if args.activate {
let locality_id = repo::marker::load(&repo_path)
.ok()
.flatten()
.map(|m| m.locality_id);
let start_options = state::session::SessionStartOptions {
mode: args.mode.map(SessionModeArg::into_state_mode),
lifecycle: state::session::SessionLifecycle::Interactive,
owner_kind: None,
actor_id: None,
supervisor_id: None,
lease_ttl_secs: None,
};
report = report.with_activation(state::session::start(
&repo_path,
args.profile.as_deref(),
locality_id.as_deref(),
start_options,
)?);
}
if cli.output == OutputFormat::Json {
let mut value = serde_json::to_value(&report)?;
if use_metadata {
value = output::strip_memory_content(value);
}
if let Some(fields) = &args.fields {
value = output::try_filter_json_fields(
value,
fields,
&output::START_FIELD_FILTER_SPEC,
)?;
}
println!("{}", serde_json::to_string_pretty(&value)?);
Ok(ExitCode::SUCCESS)
} else {
output::render_report(cli.output, &report)
}
}
}
Command::Status(args) => {
if args.fields.is_some() && cli.output != OutputFormat::Json {
anyhow::bail!("--fields requires --output json");
}
if let Some(fields) = &args.fields {
output::validate_fields(&output::STATUS_FIELD_FILTER_SPEC, fields)?;
}
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::status::run(&repo_path, args.profile.as_deref())?;
match &args.fields {
Some(fields) => output::render_report_with_fields(
report,
fields,
&output::STATUS_FIELD_FILTER_SPEC,
),
None => output::render_report(cli.output, &report),
}
}
Command::ContextCheck(args) => {
if args.fields.is_some() && cli.output != OutputFormat::Json {
anyhow::bail!("--fields requires --output json");
}
if args.memory_depth.is_some() && cli.output != OutputFormat::Json {
anyhow::bail!("--memory-depth requires --output json");
}
if let Some(fields) = &args.fields {
output::validate_fields(&output::CONTEXT_CHECK_FIELD_FILTER_SPEC, fields)?;
}
let repo_path = paths::cli::resolve(&args.path)?;
let report = state::radar::run_context_check(
&repo_path,
args.profile.as_deref(),
args.trigger.into_trigger(),
)?;
let use_metadata = args.memory_depth == Some(output::MemoryDepth::Metadata);
match &args.fields {
Some(fields) => {
let exit_code = report.exit_code();
let mut value = serde_json::to_value(&report)?;
if use_metadata {
value = output::strip_memory_content(value);
}
let filtered = output::try_filter_json_fields(
value,
fields,
&output::CONTEXT_CHECK_FIELD_FILTER_SPEC,
)?;
println!("{}", serde_json::to_string_pretty(&filtered)?);
Ok(exit_code)
}
None if use_metadata => {
let exit_code = report.exit_code();
let mut value = serde_json::to_value(&report)?;
value = output::strip_memory_content(value);
println!("{}", serde_json::to_string_pretty(&value)?);
Ok(exit_code)
}
None => output::render_report(cli.output, &report),
}
}
Command::PolicyCheck(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::policy_check::run(
&repo_path,
args.profile.as_deref(),
args.action_family.into_policy_action_family(),
)?;
output::render_report(cli.output, &report)
}
Command::Gc(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::gc::run(&repo_path)?;
output::render_report(cli.output, &report)
}
Command::Unlink(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::unlink::run(&repo_path)?;
output::render_report(cli.output, &report)
}
Command::Sync(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let mut sync_profile = commands::sync::SyncProfile::default();
if paths::git::is_git_work_tree(&repo_path) {
let profile = profile::resolve(args.profile.as_deref())?;
let layout = paths::state::StateLayout::resolve(&repo_path, profile)?;
if let Ok(config) = layout.load_profile_config() {
sync_profile.merge_config(&config.sync);
}
}
let report = commands::sync::run(&repo_path, args.check, &sync_profile)?;
output::render_report(cli.output, &report)
}
Command::Doctor(args) => {
if args.severity.is_some() && cli.output != OutputFormat::Json {
anyhow::bail!("--severity requires --output json");
}
if args.fields.is_some() && cli.output != OutputFormat::Json {
anyhow::bail!("--fields requires --output json");
}
if let Some(fields) = &args.fields {
output::validate_fields(&output::DOCTOR_FIELD_FILTER_SPEC, fields)?;
}
let repo_path = paths::cli::resolve(&args.path)?;
let mut report = commands::doctor::run(
&repo_path,
None,
commands::doctor::RunOptions {
include_repo_native_checks: !args.skip_repo_native_checks,
},
)?;
if let Some(severity) = args.severity {
let min_status = match severity {
DoctorSeverityFilter::Fail => "fail",
DoctorSeverityFilter::Warn => "warn",
DoctorSeverityFilter::Pass => "pass",
};
report.filter_by_status(min_status);
}
match &args.fields {
Some(fields) => output::render_report_with_fields(
report,
fields,
&output::DOCTOR_FIELD_FILTER_SPEC,
),
None => output::render_report(cli.output, &report),
}
}
Command::Drift(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::drift::run(&repo_path)?;
output::render_report(cli.output, &report)
}
Command::Handoff { command: sub } => match sub {
HandoffCommand::Refresh(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report =
handoff::refresh::run(&repo_path, args.profile.as_deref(), args.write)?;
output::render_report(cli.output, &report)
}
HandoffCommand::Export(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = handoff::export::run(&repo_path, args.profile.as_deref())?;
output::render_report(cli.output, &report)
}
HandoffCommand::Write(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = handoff::write::run(
&repo_path,
args.profile.as_deref(),
args.protected_write.into_state_options(),
)?;
output::render_report(cli.output, &report)
}
},
Command::Remember(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = memory::remember::run(
&repo_path,
args.profile.as_deref(),
args.scope.map(RememberScopeArg::into_scope),
args.entry_type.as_str(),
args.origin.as_str(),
args.source_ref.as_deref(),
args.dry_run,
&args.content,
)?;
output::render_report(cli.output, &report)
}
Command::Memory { command: sub } => match sub {
MemoryCommand::Search(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = memory::provider::search(
&repo_path,
args.profile.as_deref(),
&args.query,
Some(args.limit),
)?;
output::render_report(cli.output, &report)
}
MemoryCommand::Describe(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = memory::provider::describe(
&repo_path,
args.profile.as_deref(),
&args.native_id,
)?;
output::render_report(cli.output, &report)
}
MemoryCommand::Expand(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = memory::provider::expand(
&repo_path,
args.profile.as_deref(),
&args.native_id,
Some(args.limit),
)?;
output::render_report(cli.output, &report)
}
MemoryCommand::Candidate { command } => match command {
MemoryCandidateCommand::Admit(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = memory::candidate::run_admit(
&repo_path,
args.profile.as_deref(),
&args.entry,
args.source_scope.into_source_scope(),
args.destination.into_destination(),
args.write,
args.protected_write.into_state_options(),
None,
)?;
output::render_report(cli.output, &report)
}
},
MemoryCommand::Compact(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let mode = memory::compact::parse_mode(memory::compact::CompactModeRequest {
entry_id: args.entry.as_deref(),
keep_id: args.keep.as_deref(),
decay_class: args.decay_class.map(MemoryDecayClassArg::into_decay_class),
review: args.review.map(MemoryCompactReviewArg::into_review_kind),
remove: args.remove,
write: args.write,
})?;
let report = memory::compact::run(
&repo_path,
args.profile.as_deref(),
args.scope.into_scope(),
mode,
args.write,
args.protected_write.into_state_options(),
None,
)?;
output::render_report(cli.output, &report)
}
MemoryCommand::Promote(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let destination = match args.destination {
MemoryPromoteDestinationArg::BranchMemory => {
memory::promote::PromoteDestination::BranchMemory
}
MemoryPromoteDestinationArg::PodMemory => {
memory::promote::PromoteDestination::PodMemory
}
MemoryPromoteDestinationArg::RepoMemory => {
memory::promote::PromoteDestination::RepoMemory
}
MemoryPromoteDestinationArg::ProfileMemory => {
memory::promote::PromoteDestination::ProfileMemory
}
MemoryPromoteDestinationArg::ProjectTruth => {
let target_file = args.target_file.ok_or_else(|| {
anyhow::anyhow!(
"`--destination project-truth` requires `--target-file <path>`"
)
})?;
memory::promote::PromoteDestination::ProjectTruth { target_file }
}
};
let report = memory::promote::run(
&repo_path,
args.profile.as_deref(),
&args.entry,
args.write,
args.source_outcome
.map(MemoryPromoteSourceOutcomeArg::into_source_outcome),
destination,
args.protected_write.into_state_options(),
None,
)?;
output::render_report(cli.output, &report)
}
},
Command::RuntimeState { command: sub } => match sub {
RuntimeStateCommand::Export(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = state::runtime_export::run(
&repo_path,
args.profile.as_deref(),
args.projection_target
.map(ProjectionTargetArg::into_projection_target),
args.projection_format
.map(ProjectionFormatArg::into_projection_format),
)?;
output::render_report(cli.output, &report)
}
RuntimeStateCommand::ChildBootstrap(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = state::child_bootstrap::run(&repo_path, args.profile.as_deref())?;
output::render_report(cli.output, &report)
}
},
Command::EscalationState { command: sub } => match sub {
EscalationStateCommand::Set(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::escalation::set(
&repo_path,
args.base.profile.as_deref(),
args.id.as_deref(),
args.protected_write.into_state_options(),
Some(args.kind.into_kind()),
&args.reason,
)?;
output::render_report(cli.output, &report)
}
EscalationStateCommand::Clear(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::escalation::clear(
&repo_path,
args.base.profile.as_deref(),
args.id.as_deref(),
args.protected_write.into_state_options(),
)?;
output::render_report(cli.output, &report)
}
EscalationStateCommand::List(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::escalation::list(&repo_path, args.base.profile.as_deref())?;
output::render_report(cli.output, &report)
}
},
Command::RadarState(args) => {
if args.fields.is_some() && cli.output != OutputFormat::Json {
anyhow::bail!("--fields requires --output json");
}
if args.memory_depth.is_some() && cli.output != OutputFormat::Json {
anyhow::bail!("--memory-depth requires --output json");
}
if args.commit && cli.output != OutputFormat::Json {
anyhow::bail!("--commit requires --output json");
}
if args.since_session.is_some() && cli.output != OutputFormat::Json {
anyhow::bail!("--since-session requires --output json");
}
if let Some(fields) = &args.fields {
output::validate_fields(&output::RADAR_STATE_FIELD_FILTER_SPEC, fields)?;
}
let repo_path = paths::cli::resolve(&args.path)?;
let needs_digests = args.since_session.is_some();
let (report, current_digests) = if needs_digests {
let (r, d) = state::radar::run_with_digests(&repo_path, args.profile.as_deref(), false)?;
(r, Some(d))
} else {
(state::radar::run(&repo_path, args.profile.as_deref(), false)?, None)
};
let use_metadata = args.memory_depth == Some(output::MemoryDepth::Metadata);
let needs_json = args.fields.is_some() || args.commit || use_metadata || args.since_session.is_some();
if needs_json {
let mut value = serde_json::to_value(&report)?;
if let Some(ref since) = args.since_session {
value = apply_since_session_delta(
value,
&repo_path,
args.profile.as_deref(),
since,
current_digests.as_ref().expect("digests available when --since-session is set"),
)?;
}
if args.commit {
let mut buf = String::new();
std::io::stdin()
.read_to_string(&mut buf)
.context("failed to read commit payload from stdin")?;
let payload: state::radar::CommitPayload =
serde_json::from_str(&buf)
.context("failed to parse commit payload JSON")?;
let commit_result = state::radar::commit_writes(
&repo_path,
args.profile.as_deref(),
payload,
);
value["commit"] = serde_json::to_value(&commit_result)?;
}
if use_metadata {
value = output::strip_memory_content(value);
}
if let Some(fields) = &args.fields {
value = output::try_filter_json_fields(
value,
fields,
&output::RADAR_STATE_FIELD_FILTER_SPEC,
)?;
}
println!("{}", serde_json::to_string_pretty(&value)?);
Ok(ExitCode::SUCCESS)
} else {
output::render_report(cli.output, &report)
}
}
Command::Checkpoint(args) => {
if args.fields.is_some() && cli.output != OutputFormat::Json {
anyhow::bail!("--fields requires --output json");
}
if args.memory_depth.is_some() && cli.output != OutputFormat::Json {
anyhow::bail!("--memory-depth requires --output json");
}
if args.commit && cli.output != OutputFormat::Json {
anyhow::bail!("--commit requires --output json");
}
if args.since_session.is_some() && cli.output != OutputFormat::Json {
anyhow::bail!("--since-session requires --output json");
}
if let Some(fields) = &args.fields {
output::validate_fields(&output::RADAR_STATE_FIELD_FILTER_SPEC, fields)?;
}
let repo_path = paths::cli::resolve(&args.path)?;
let needs_digests = args.since_session.is_some();
let (report, current_digests) = if needs_digests {
let (r, d) = state::radar::run_with_digests(&repo_path, args.profile.as_deref(), true)?;
(r, Some(d))
} else {
(state::radar::run(&repo_path, args.profile.as_deref(), true)?, None)
};
let use_metadata = args.memory_depth == Some(output::MemoryDepth::Metadata);
let needs_json = args.fields.is_some() || args.commit || use_metadata || args.since_session.is_some();
if needs_json {
let mut value = serde_json::to_value(&report)?;
if let Some(ref since) = args.since_session {
value = apply_since_session_delta(
value,
&repo_path,
args.profile.as_deref(),
since,
current_digests.as_ref().expect("digests available when --since-session is set"),
)?;
}
if args.commit {
let mut buf = String::new();
std::io::stdin()
.read_to_string(&mut buf)
.context("failed to read commit payload from stdin")?;
let payload: state::radar::CommitPayload =
serde_json::from_str(&buf)
.context("failed to parse commit payload JSON")?;
let commit_result = state::radar::commit_writes(
&repo_path,
args.profile.as_deref(),
payload,
);
value["commit"] = serde_json::to_value(&commit_result)?;
}
if use_metadata {
value = output::strip_memory_content(value);
}
if let Some(fields) = &args.fields {
value = output::try_filter_json_fields(
value,
fields,
&output::RADAR_STATE_FIELD_FILTER_SPEC,
)?;
}
println!("{}", serde_json::to_string_pretty(&value)?);
Ok(ExitCode::SUCCESS)
} else {
output::render_report(cli.output, &report)
}
}
Command::Recovery { command: sub } => match sub {
RecoveryCommand::Write(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = recovery::write(
&repo_path,
args.profile.as_deref(),
args.protected_write.into_state_options(),
)?;
output::render_report(cli.output, &report)
}
},
Command::Session { command: sub } => match sub {
SessionCommand::Open(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::session_open::run(
&repo_path,
args.profile.as_deref(),
args.worktree.as_deref(),
args.branch.as_deref(),
args.from_ref.as_deref(),
args.pod.as_deref(),
)?;
output::render_report(cli.output, &report)
}
},
Command::SessionState { command: sub } => match sub {
SessionStateCommand::Start(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let locality_id = repo::marker::load(&repo_path)
.ok()
.flatten()
.map(|m| m.locality_id);
let start_options = state::session::SessionStartOptions {
mode: args.mode.map(SessionModeArg::into_state_mode),
lifecycle: args.lifecycle.into_state_lifecycle(),
owner_kind: args
.owner_kind
.map(AutonomousOwnerKindArg::into_state_owner_kind),
actor_id: args.actor_id,
supervisor_id: args.supervisor_id,
lease_ttl_secs: args.lease_seconds,
};
let report = state::session::start(
&repo_path,
args.base.profile.as_deref(),
locality_id.as_deref(),
start_options,
)?;
output::render_report(cli.output, &report)
}
SessionStateCommand::Heartbeat(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::session::heartbeat(
&repo_path,
args.base.profile.as_deref(),
state::session::SessionHeartbeatOptions {
actor_id: args.actor_id,
activity: args.activity,
},
)?;
output::render_report(cli.output, &report)
}
SessionStateCommand::Clear(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let locality_id = repo::marker::load(&repo_path)
.ok()
.flatten()
.map(|m| m.locality_id);
let report = state::session::clear(
&repo_path,
args.base.profile.as_deref(),
locality_id.as_deref(),
state::session::SessionClearOptions {
actor_id: args.actor_id,
reason: args.reason,
},
)?;
output::render_report(cli.output, &report)
}
SessionStateCommand::Takeover(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let locality_id = repo::marker::load(&repo_path)
.ok()
.flatten()
.map(|m| m.locality_id);
let report = state::session::takeover(
&repo_path,
args.base.profile.as_deref(),
locality_id.as_deref(),
state::session::SessionTakeoverOptions {
actor_id: args.actor_id,
supervisor_id: args.supervisor_id,
reason: args.reason,
},
)?;
output::render_report(cli.output, &report)
}
SessionStateCommand::Gates { command } => match command {
SessionGateCommand::List(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = state::session_gates::list(&repo_path, args.profile.as_deref())?;
output::render_report(cli.output, &report)
}
SessionGateCommand::Replace(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::session_gates::replace(
&repo_path,
args.base.profile.as_deref(),
args.gates,
args.protected_write.into_state_options(),
)?;
output::render_report(cli.output, &report)
}
SessionGateCommand::Seed(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::session_gates::seed(
&repo_path,
args.base.profile.as_deref(),
args.from.into_state_source(),
args.protected_write.into_state_options(),
)?;
output::render_report(cli.output, &report)
}
SessionGateCommand::SetStatus(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::session_gates::set_status(
&repo_path,
args.base.profile.as_deref(),
args.index,
args.status.into_status(),
args.protected_write.into_state_options(),
)?;
output::render_report(cli.output, &report)
}
SessionGateCommand::Advance(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::session_gates::advance(
&repo_path,
args.base.profile.as_deref(),
args.protected_write.into_state_options(),
)?;
output::render_report(cli.output, &report)
}
SessionGateCommand::Clear(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::session_gates::clear(
&repo_path,
args.base.profile.as_deref(),
args.protected_write.into_state_options(),
)?;
output::render_report(cli.output, &report)
}
},
},
Command::Hooks { command: sub } => match sub {
HooksCommand::Install(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::hooks::install(&repo_path, args.force)?;
output::render_report(cli.output, &report)
}
HooksCommand::Check(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::hooks::check(&repo_path)?;
output::render_report(cli.output, &report)
}
},
Command::Skills { command: sub } => match sub {
SkillsCommand::Install(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let global = !args.local;
let report = commands::skills::run(&repo_path, global)?;
output::render_report(cli.output, &report)
}
},
Command::Pod { command: sub } => match sub {
PodCommand::Init(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::pod::init(
&repo_path,
args.profile.as_deref(),
&args.name,
args.force,
)?;
output::render_report(cli.output, &report)
}
PodCommand::List => {
let report = commands::pod::list()?;
output::render_report(cli.output, &report)
}
PodCommand::Status(args) => {
let repo_path = args
.path
.as_ref()
.map(|p| paths::cli::resolve(p))
.transpose()?;
let report = commands::pod::status(
args.name.as_deref(),
repo_path.as_deref(),
args.profile.as_deref(),
)?;
output::render_report(cli.output, &report)
}
PodCommand::MigrateDefaults(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = memory::pod_migration::run(
&repo_path,
args.profile.as_deref(),
&args.memory_items,
&args.policy_items,
args.adopt_suggested,
args.write,
)?;
output::render_report(cli.output, &report)
}
},
Command::McpServe => {
mcp::serve()?;
Ok(ExitCode::SUCCESS)
}
#[cfg(feature = "daemon")]
Command::Daemon(args) => {
let rt = tokio::runtime::Runtime::new()
.map_err(|e| anyhow::anyhow!("failed to create tokio runtime: {e}"))?;
rt.block_on(mcp::daemon::serve(
&args.host,
args.port,
args.profile,
args.cors,
))?;
Ok(ExitCode::SUCCESS)
}
}
}