use serde::Serialize;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum CommandAccess {
ReadOnly,
Mutating,
Conditional,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum StateWriteMode {
Never,
Always,
WhenFlagSet,
WhenFlagUnset,
ImplicitRuntimeDecision,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ControlFlagEffect {
ApplyChanges,
PreviewOnly,
ConfirmWrite,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub(crate) struct ControlFlagDescriptor {
pub(crate) name: &'static str,
pub(crate) effect: ControlFlagEffect,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub(crate) struct CommandBehavior {
pub(crate) access: CommandAccess,
pub(crate) state_write_mode: StateWriteMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) control_flag: Option<ControlFlagDescriptor>,
#[serde(skip_serializing_if = "is_false")]
pub(crate) supports_fields: bool,
#[serde(skip_serializing_if = "is_false")]
pub(crate) compact_mcp_default: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) mcp_tool: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) mcp_command: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) note: Option<&'static str>,
}
const fn is_false(value: &bool) -> bool {
!*value
}
impl CommandBehavior {
const fn new(access: CommandAccess, state_write_mode: StateWriteMode) -> Self {
Self {
access,
state_write_mode,
control_flag: None,
supports_fields: false,
compact_mcp_default: false,
mcp_tool: None,
mcp_command: None,
note: None,
}
}
const fn with_control_flag(mut self, name: &'static str, effect: ControlFlagEffect) -> Self {
self.control_flag = Some(ControlFlagDescriptor { name, effect });
self
}
const fn with_fields(mut self) -> Self {
self.supports_fields = true;
self
}
const fn with_compact_mcp_default(mut self) -> Self {
self.compact_mcp_default = true;
self
}
const fn with_mcp(mut self, tool: &'static str, command: &'static str) -> Self {
self.mcp_tool = Some(tool);
self.mcp_command = Some(command);
self
}
const fn with_note(mut self, note: &'static str) -> Self {
self.note = Some(note);
self
}
}
const fn read_only() -> CommandBehavior {
CommandBehavior::new(CommandAccess::ReadOnly, StateWriteMode::Never)
}
const fn mutating_always() -> CommandBehavior {
CommandBehavior::new(CommandAccess::Mutating, StateWriteMode::Always)
}
const fn preview_by_default(flag: &'static str) -> CommandBehavior {
CommandBehavior::new(CommandAccess::Mutating, StateWriteMode::WhenFlagSet)
.with_control_flag(flag, ControlFlagEffect::ApplyChanges)
}
const fn confirm_before_write(flag: &'static str) -> CommandBehavior {
CommandBehavior::new(CommandAccess::Mutating, StateWriteMode::WhenFlagSet)
.with_control_flag(flag, ControlFlagEffect::ConfirmWrite)
}
const fn writes_unless_preview_flag(flag: &'static str) -> CommandBehavior {
CommandBehavior::new(CommandAccess::Mutating, StateWriteMode::WhenFlagUnset)
.with_control_flag(flag, ControlFlagEffect::PreviewOnly)
}
const fn conditional_write(flag: &'static str) -> CommandBehavior {
CommandBehavior::new(CommandAccess::Conditional, StateWriteMode::WhenFlagSet)
.with_control_flag(flag, ControlFlagEffect::ApplyChanges)
}
const fn runtime_conditional() -> CommandBehavior {
CommandBehavior::new(
CommandAccess::Conditional,
StateWriteMode::ImplicitRuntimeDecision,
)
}
pub(crate) fn for_command_path(path: &[&str]) -> Option<CommandBehavior> {
match path {
["describe"] => Some(read_only()),
["attach"] => Some(
mutating_always()
.with_mcp("ccd_repo", "attach")
.with_note("bootstraps profile, marker, project overlay, and workspace-local state"),
),
["scaffold"] => Some(
mutating_always()
.with_mcp("ccd_repo", "scaffold")
.with_note("writes starter project-truth files; existing files require --force to overwrite"),
),
["repo", "status"] => Some(read_only()),
["repo", "list"] => Some(read_only()),
["repo", "relink"] => Some(
mutating_always()
.with_note("updates the current workspace marker and relinks it to an existing project ID"),
),
["repo", "merge"] => Some(
confirm_before_write("force").with_note(
"destructive repair: merges two project identities and removes the source registry entry",
),
),
["repo", "split"] => Some(
mutating_always()
.with_note("creates a new project ID for the current workspace and copies project-overlay state"),
),
["link"] => Some(
mutating_always()
.with_mcp("ccd_repo", "link")
.with_note("links the current workspace to a project ID and updates registry metadata"),
),
["check"] => Some(read_only().with_mcp("ccd_health", "check")),
["preflight"] => Some(read_only().with_mcp("ccd_health", "preflight")),
["start"] => Some(
conditional_write("refresh")
.with_fields()
.with_compact_mcp_default()
.with_mcp("ccd_session", "start")
.with_note("default start is read-only; --refresh updates workspace-local continuity and cached next-step context; --activate adds session telemetry for the one-call raw CLI startup path; use --fields or MCP compact defaults to trim machine-readable output"),
),
["status"] => Some(
read_only()
.with_fields()
.with_note("use --fields with --output json to request only the status surfaces you need"),
),
["context-check"] => Some(
read_only()
.with_mcp("ccd_context", "context-check")
.with_note("evaluates the mid-session refresh contract without mutating continuity, memory, or escalation state"),
),
["policy-check"] => Some(read_only().with_mcp("ccd_state", "policy-check")),
["gc"] => Some(mutating_always().with_mcp("ccd_repo", "gc")),
["unlink"] => Some(mutating_always().with_mcp("ccd_repo", "unlink")),
["sync"] => Some(
writes_unless_preview_flag("check")
.with_mcp("ccd_health", "sync")
.with_note("default sync writes generated mirrors; --check is read-only"),
),
["doctor"] => Some(
read_only()
.with_fields()
.with_mcp("ccd_health", "doctor")
.with_note("use --severity or --fields with --output json to trim machine-readable doctor output"),
),
["drift"] => Some(read_only().with_mcp("ccd_health", "drift")),
["handoff", "refresh"] => Some(
preview_by_default("write")
.with_mcp("ccd_state", "handoff-refresh")
.with_note("preview is default; --write persists the refreshed handoff/export"),
),
["handoff", "export"] => Some(read_only()),
["handoff", "write"] => Some(mutating_always()),
["remember"] => Some(
writes_unless_preview_flag("dry-run")
.with_note("default remember writes memory; --dry-run previews the candidate entry"),
),
["memory", "search"] => Some(
read_only()
.with_mcp("ccd_memory_recall", "memory-search")
.with_note("provider recall stays additive and falls back explicitly to authored memory when external recall is unavailable"),
),
["memory", "describe"] => Some(
read_only()
.with_mcp("ccd_memory_recall", "memory-describe")
.with_note("describe returns the normalized recall envelope for the selected provider-native id"),
),
["memory", "expand"] => Some(
read_only()
.with_mcp("ccd_memory_recall", "memory-expand")
.with_note("expand returns adjacent or provider-suggested recall context for the selected provider-native id"),
),
["memory", "compact"] => Some(
preview_by_default("write")
.with_mcp("ccd_memory", "memory-compact")
.with_note("preview is default; --write applies compaction changes"),
),
["memory", "candidate", "admit"] => Some(
preview_by_default("write")
.with_mcp("ccd_memory", "memory-candidate-admit")
.with_note("preview is default; --write stages the reviewed candidate as a higher-scope `promotion_candidate` entry"),
),
["memory", "promote"] => Some(
preview_by_default("write")
.with_mcp("ccd_memory", "memory-promote")
.with_note("preview is default; --write applies the promotion"),
),
["runtime-state", "export"] => Some(
read_only()
.with_compact_mcp_default()
.with_mcp("ccd_state", "runtime-state-export"),
),
["runtime-state", "child-bootstrap"] => {
Some(read_only().with_mcp("ccd_delegation", "child-bootstrap"))
}
["escalation-state", "set"] => {
Some(mutating_always().with_mcp("ccd_escalation", "set-escalation"))
}
["escalation-state", "clear"] => {
Some(mutating_always().with_mcp("ccd_escalation", "clear-escalation"))
}
["escalation-state", "list"] => {
Some(read_only().with_mcp("ccd_escalation", "list-escalations"))
}
["radar-state"] => Some(
runtime_conditional()
.with_mcp("ccd_state", "radar-state")
.with_note("may update workspace-local planning state when wrap-up or continuity drift requires it"),
),
["checkpoint"] => Some(
runtime_conditional()
.with_mcp("ccd_state", "checkpoint")
.with_note(
"lightweight checkpoint: skips full evaluation, candidate updates, workflow hints, and approval steps",
),
),
["recovery", "write"] => Some(mutating_always().with_mcp("ccd_recovery", "write-recovery")),
["session", "open"] => Some(mutating_always().with_mcp("ccd_session", "session-open")),
["session-state", "start"] => Some(
mutating_always().with_mcp("ccd_session_lifecycle", "start-session"),
),
["session-state", "heartbeat"] => Some(
mutating_always()
.with_mcp("ccd_session_lifecycle", "heartbeat-session")
.with_note("renews an autonomous workspace-local session lease for the current actor"),
),
["session-state", "clear"] => Some(
mutating_always().with_mcp("ccd_session_lifecycle", "clear-session"),
),
["session-state", "takeover"] => Some(
mutating_always()
.with_mcp("ccd_session_lifecycle", "takeover-session")
.with_note("adopts a stale autonomous workspace-local session for a different actor"),
),
["session-state", "gates", "list"] => {
Some(read_only().with_mcp("ccd_session_gates", "list-gates"))
}
["session-state", "gates", "replace"] => {
Some(mutating_always().with_mcp("ccd_session_gates", "replace-gates"))
}
["session-state", "gates", "seed"] => {
Some(mutating_always().with_mcp("ccd_session_gates", "seed-gates"))
}
["session-state", "gates", "set-status"] => Some(
mutating_always().with_mcp("ccd_session_gates", "set-gate-status"),
),
["session-state", "gates", "advance"] => {
Some(mutating_always().with_mcp("ccd_session_gates", "advance-gates"))
}
["session-state", "gates", "clear"] => {
Some(mutating_always().with_mcp("ccd_session_gates", "clear-gates"))
}
["hooks", "install"] => Some(mutating_always().with_mcp("ccd_health", "hooks-install")),
["hooks", "check"] => Some(read_only().with_mcp("ccd_health", "hooks-check")),
["skills", "install"] => Some(mutating_always().with_mcp("ccd_repo", "skills-install")),
["pod", "init"] => Some(mutating_always()),
["pod", "list"] => Some(read_only()),
["pod", "status"] => Some(read_only()),
["pod", "migrate-defaults"] => Some(
preview_by_default("write").with_note(
"preview is default; --write moves explicitly selected or suggested profile defaults into pod memory and policy",
),
),
["backlog", "pull"] => Some(mutating_always()),
["backlog", "scope"] => Some(mutating_always()),
["backlog", "next"] => Some(read_only()),
["backlog", "claim"] => Some(
preview_by_default("write")
.with_note("preview is default; --write executes the active backlog adapter mutation"),
),
["backlog", "set-status"] => Some(
preview_by_default("write")
.with_note("preview is default; --write executes the active backlog adapter mutation"),
),
["backlog", "complete"] => Some(
preview_by_default("write")
.with_note("preview is default; --write executes the active backlog adapter mutation"),
),
["backlog", "adapters"] => Some(read_only()),
["backlog", "promote-next"] => Some(
preview_by_default("write")
.with_note("preview is default; --write applies promotion through the active adapter"),
),
["backlog", "bootstrap-github"] => Some(mutating_always().with_mcp(
"ccd_backlog",
"backlog-bootstrap-github",
)),
["backlog", "pull-github"] => Some(
mutating_always().with_mcp("ccd_backlog", "backlog-pull-github"),
),
["backlog", "lint"] => Some(read_only().with_mcp("ccd_backlog", "backlog-lint")),
["backlog", "groom"] => Some(read_only().with_mcp("ccd_backlog", "backlog-groom")),
["codemap", "import"] => Some(mutating_always()),
["codemap", "status"] => Some(read_only().with_mcp("ccd_codemap", "codemap-status")),
["codemap", "query"] => Some(read_only().with_mcp("ccd_codemap", "codemap-query")),
_ => None,
}
}