mod behavioral_drift;
mod candidates;
mod context_check;
mod context_health;
mod evaluation;
mod session_boundary;
mod status;
pub(crate) use status::{MemoryStatus, NextStepStatus, WrapUpStatus};
use std::collections::BTreeMap;
use std::path::Path;
use std::process::ExitCode;
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
use crate::commands::check;
use crate::db;
use crate::handoff::{self, BranchMode, GitState};
use crate::memory::entries as memory_entries;
use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile::{self, DEFAULT_PROFILE};
use crate::repo::marker as repo_marker;
use crate::session_boundary::SessionBoundaryRecommendation;
use crate::state::compiled as compiled_state;
use crate::state::consistency;
use crate::state::escalation as escalation_state;
use crate::state::projection_metadata;
use crate::state::runtime as runtime_state;
use crate::state::session as session_state;
use crate::state::session_gates;
use crate::state::work_stream_decay;
use crate::telemetry::{cost as telemetry_cost, host as host_telemetry};
use tracing::{debug, info_span};
const RADAR_STATE_SCHEMA_VERSION: u32 = 22;
const CONTEXT_CHECK_SCHEMA_VERSION: u32 = 1;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ContextCheckTrigger {
Interval,
Manual,
PreCompaction,
IdleReset,
Resume,
SupervisorPoll,
}
impl ContextCheckTrigger {
fn as_str(self) -> &'static str {
match self {
Self::Interval => "interval",
Self::Manual => "manual",
Self::PreCompaction => "pre_compaction",
Self::IdleReset => "idle_reset",
Self::Resume => "resume",
Self::SupervisorPoll => "supervisor_poll",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ContextCheckAction {
None,
InjectDelta,
CheckpointNow,
FlushForCompaction,
WrapUpRequired,
Escalate,
}
impl ContextCheckAction {
fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::InjectDelta => "inject_delta",
Self::CheckpointNow => "checkpoint_now",
Self::FlushForCompaction => "flush_for_compaction",
Self::WrapUpRequired => "wrap_up_required",
Self::Escalate => "escalate",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ContextCheckUrgency {
Low,
Moderate,
High,
Critical,
}
impl ContextCheckUrgency {
fn as_str(self) -> &'static str {
match self {
Self::Low => "low",
Self::Moderate => "moderate",
Self::High => "high",
Self::Critical => "critical",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum DriftAggregateStatus {
Aligned,
NeedsRecalibration,
NoSignal,
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum DriftSignalStatus {
Aligned,
Drift,
NoSignal,
}
#[derive(Serialize)]
pub struct RadarStateReport {
command: &'static str,
mode: &'static str,
schema_version: u32,
ok: bool,
path: String,
profile: String,
project_id: String,
locality_id: String,
handoff: HandoffState,
execution_gates: session_gates::ExecutionGatesView,
effective_memory: MemoryState,
repo_native_checks: RepoNativeChecksState,
git: Option<GitState>,
checkout_state: Option<handoff::CheckoutStateView>,
session_state: RadarSessionStateView,
recovery: runtime_state::RuntimeRecoveryView,
escalation: escalation_state::EscalationView,
projection_telemetry: ProjectionTelemetryView,
work_stream_decay: work_stream_decay::WorkStreamDecayView,
context_health: ContextHealth,
session_boundary: SessionBoundaryRecommendation,
behavioral_drift: BehavioralDriftState,
evaluation: RadarEvaluation,
candidate_updates: CandidateUpdates,
approval_intents: Vec<&'static str>,
approval_steps: Vec<ApprovalStep>,
}
#[derive(Serialize)]
pub struct ContextCheckReport {
command: &'static str,
schema_version: u32,
ok: bool,
path: String,
profile: String,
project_id: String,
locality_id: String,
trigger: ContextCheckTrigger,
action: ContextCheckAction,
reason: &'static str,
recommendation: String,
urgency: ContextCheckUrgency,
throttle: ContextCheckThrottle,
payload: ContextCheckPayload,
#[serde(skip_serializing_if = "Vec::is_empty")]
evidence: Vec<String>,
session_state: RadarSessionStateView,
execution_gates: session_gates::ExecutionGatesView,
recovery: runtime_state::RuntimeRecoveryView,
escalation: escalation_state::EscalationView,
context_health: ContextHealth,
session_boundary: SessionBoundaryRecommendation,
behavioral_drift: BehavioralDriftState,
}
#[derive(Serialize)]
pub(crate) struct ContextCheckThrottle {
suppressed: bool,
next_interval_hint_seconds: u64,
}
#[derive(Serialize)]
pub(crate) struct ContextCheckPayload {
policy_digest: String,
focus_digest: String,
state_delta: String,
pub(crate) risk_summary: String,
}
#[derive(Serialize)]
pub(crate) struct HandoffState {
path: String,
pub(crate) title: String,
pub(crate) immediate_actions: Vec<String>,
current_system_state: Vec<String>,
pub(crate) definition_of_done: Vec<String>,
}
#[derive(Serialize)]
pub(crate) struct RadarSessionStateView {
path: String,
status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) mode: Option<session_state::SessionMode>,
#[serde(skip_serializing_if = "Option::is_none")]
start_count: Option<u32>,
#[serde(flatten)]
lifecycle: session_state::SessionLifecycleProjection,
}
#[derive(Serialize)]
pub(crate) struct ProjectionTelemetryView {
source_fingerprint: String,
delta_status: &'static str,
delta: compiled_state::ProjectionDeltaStats,
token_counts: compiled_state::ProjectionTokenTelemetry,
cadence: ProjectionCadenceView,
}
#[derive(Serialize)]
struct ProjectionCadenceView {
target: &'static str,
current: BTreeMap<String, compiled_state::ProjectionFormatCadenceCurrent>,
history: BTreeMap<String, compiled_state::ProjectionFormatCadenceSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
provider_cache_usage: Option<ProjectionProviderCacheUsageView>,
}
#[derive(Serialize)]
struct ProjectionProviderCacheUsageView {
#[serde(skip_serializing_if = "Option::is_none")]
cache_creation_input_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
cache_read_input_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
blended_total_tokens: Option<u64>,
}
#[derive(Serialize)]
pub(crate) struct MemoryState {
status: &'static str,
profile_path: String,
project_path: String,
locality_path: String,
pod_path: String,
work_stream_path: String,
branch_path: String,
workspace_path: String,
clone_path: String,
pub(crate) content: String,
pub(crate) structured: memory_entries::StructuredMemoryView,
#[serde(skip_serializing)]
pub(crate) contributing_paths: Vec<String>,
}
#[derive(Serialize)]
pub(crate) struct RepoNativeChecksState {
surface: check::CheckSurfaceView,
pub(crate) failures: usize,
pub(crate) checks: Vec<RepoNativeCheckResult>,
}
#[derive(Serialize)]
pub(crate) struct RepoNativeCheckResult {
pub(crate) id: String,
path: String,
pub(crate) status: &'static str,
pub(crate) severity: &'static str,
pub(crate) message: String,
duration_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
exit_code: Option<i32>,
}
#[derive(Serialize)]
pub(crate) struct ContextHealth {
source: &'static str,
context_used_pct: Option<u8>,
degradation_risk_pct: Option<u8>,
pub(crate) band: &'static str,
confidence: &'static str,
signals: ContextSignals,
cost: telemetry_cost::TelemetryCostView,
pub(crate) recommendation: &'static str,
}
#[derive(Serialize)]
pub(crate) struct ContextSignals {
pub(crate) compacted: Option<bool>,
pub(crate) session_minutes: Option<u64>,
pub(crate) ccd_start_cycles_in_conversation: Option<u32>,
pub(crate) host_total_tokens: Option<u64>,
pub(crate) host_context_window_tokens: Option<u64>,
pub(crate) tool_output_kb: Option<u64>,
pub(crate) operator_symptoms: Vec<String>,
}
#[derive(Serialize)]
pub(crate) struct BehavioralDriftState {
pub(crate) status: DriftAggregateStatus,
pub(crate) summary: String,
pub(crate) evidence: Vec<String>,
recommended_corrections: Vec<String>,
signals: Vec<BehavioralDriftSignal>,
}
#[derive(Serialize)]
pub(crate) struct BehavioralDriftSignal {
pub(crate) id: &'static str,
pub(crate) status: DriftSignalStatus,
pub(crate) summary: String,
pub(crate) evidence: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) recommended_correction: Option<String>,
}
pub(crate) struct BehavioralDriftInputs<'a> {
pub(crate) layout: &'a StateLayout,
pub(crate) locality_id: &'a str,
pub(crate) git: Option<&'a GitState>,
pub(crate) handoff: &'a HandoffState,
pub(crate) handoff_contents: &'a str,
pub(crate) runtime: &'a runtime_state::LoadedRuntimeState,
pub(crate) runtime_handoff: &'a runtime_state::RuntimeHandoffState,
pub(crate) tracked_session: Option<&'a session_state::SessionStateFile>,
pub(crate) consistency_axes: Vec<consistency::ConsistencyAxis>,
}
pub(crate) struct EvaluationInputs<'a> {
pub(crate) git: Option<&'a GitState>,
pub(crate) candidates: &'a CandidateUpdates,
pub(crate) execution_gates: &'a session_gates::ExecutionGatesView,
pub(crate) effective_memory: &'a MemoryState,
pub(crate) repo_native_checks: &'a RepoNativeChecksState,
pub(crate) behavioral_drift: &'a BehavioralDriftState,
pub(crate) escalation_view: &'a escalation_state::EscalationView,
}
struct LoadedHandoffState {
handoff: HandoffState,
contents: String,
}
struct RadarSnapshot {
report: RadarStateReport,
policy_digest: String,
projection_digests: compiled_state::ProjectionDigests,
}
#[derive(Serialize)]
pub(crate) struct RadarEvaluation {
pub(crate) next_step: EvaluationBucket<NextStepStatus>,
pub(crate) memory: EvaluationBucket<MemoryStatus>,
pub(crate) wrap_up: EvaluationBucket<WrapUpStatus>,
}
#[derive(Serialize)]
pub(crate) struct EvaluationBucket<S: Serialize> {
pub(crate) status: S,
pub(crate) summary: String,
pub(crate) evidence: Vec<String>,
}
#[derive(Serialize)]
pub(crate) struct CandidateUpdates {
pub(crate) handoff: Vec<CandidateUpdate>,
pub(crate) memory: Vec<CandidateUpdate>,
}
#[derive(Serialize)]
pub(crate) struct CandidateUpdate {
pub(crate) kind: &'static str,
pub(crate) summary: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) section: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) replacement_lines: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) entry_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) source_scope: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) suggested_destination: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) action: Option<CandidateAction>,
}
#[derive(Serialize)]
pub(crate) struct CandidateAction {
pub(crate) kind: &'static str,
pub(crate) argv: Vec<String>,
pub(crate) note: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) apply: Option<CandidateActionApply>,
}
#[derive(Serialize)]
pub(crate) struct CandidateActionApply {
pub(crate) argv: Vec<String>,
pub(crate) requirements: Vec<CandidateActionRequirement>,
pub(crate) note: &'static str,
}
#[derive(Serialize)]
pub(crate) struct CandidateActionRequirement {
pub(crate) id: &'static str,
pub(crate) flag: &'static str,
pub(crate) value_type: &'static str,
pub(crate) required: bool,
pub(crate) allowed_values: Vec<&'static str>,
pub(crate) note: &'static str,
}
#[derive(Serialize)]
pub(crate) struct ApprovalStep {
pub(crate) id: &'static str,
pub(crate) question: &'static str,
pub(crate) recommended_answer: &'static str,
pub(crate) recommendation: String,
pub(crate) evidence: Vec<String>,
}
impl RadarEvaluation {
fn default_empty() -> Self {
Self {
next_step: EvaluationBucket {
status: NextStepStatus::NoChange,
summary: String::new(),
evidence: Vec::new(),
},
memory: EvaluationBucket {
status: MemoryStatus::NoChange,
summary: String::new(),
evidence: Vec::new(),
},
wrap_up: EvaluationBucket {
status: WrapUpStatus::NoChange,
summary: String::new(),
evidence: Vec::new(),
},
}
}
}
impl CandidateUpdates {
fn default_empty() -> Self {
Self {
handoff: Vec::new(),
memory: Vec::new(),
}
}
}
impl RepoNativeChecksState {
fn default_empty(repo_root: &Path) -> Self {
let surface_path = repo_root.join(check::COMMANDS_DIR);
Self {
surface: check::CheckSurfaceView {
path: surface_path.display().to_string(),
status: "skipped",
prefix: check::CHECK_PREFIX,
entries: Vec::new(),
note: None,
},
failures: 0,
checks: Vec::new(),
}
}
}
impl CommandReport for RadarStateReport {
fn exit_code(&self) -> ExitCode {
ExitCode::SUCCESS
}
fn render_text(&self) {
println!("Handoff: {}", self.handoff.title);
if !self.handoff.immediate_actions.is_empty() {
println!("Immediate actions:");
for (index, step) in self.handoff.immediate_actions.iter().enumerate() {
println!("{}. {step}", index + 1);
}
}
if let Some(git) = &self.git {
println!("Workspace: {}", radar_git_summary(git));
if !git.staged_files.is_empty()
|| !git.unstaged_files.is_empty()
|| !git.untracked_files.is_empty()
{
println!("Workspace changes:");
for line in radar_git_changes(git) {
println!("- {line}");
}
}
} else {
println!("Workspace: unavailable by design for directory-substrate workspaces");
}
if let Some(checkout_state) = self.checkout_state.as_ref().filter(|state| state.advisory) {
println!("Workspace note: {}", checkout_state.summary);
}
println!("Context health: {}", self.context_health.band);
if let Some(cost_summary) = radar_cost_summary(&self.context_health.cost) {
println!("Cost: {cost_summary}");
}
if !self.evaluation.wrap_up.summary.is_empty() {
println!(
"Wrap-up: {} [{}]",
self.evaluation.wrap_up.summary,
self.evaluation.wrap_up.status.as_str()
);
}
println!(
"Recommendation: {} - {}",
self.session_boundary.action.as_str(),
self.session_boundary.summary
);
let findings = radar_material_findings(self);
if !findings.is_empty() {
println!("Material findings:");
for finding in findings {
println!("- {finding}");
}
}
}
}
fn radar_git_summary(git: &GitState) -> String {
let tracking = match (git.upstream.as_deref(), git.ahead, git.behind) {
(Some(upstream), 0, 0) => format!("tracking {upstream} in sync"),
(Some(upstream), ahead, 0) => format!("tracking {upstream}, ahead {ahead}"),
(Some(upstream), 0, behind) => format!("tracking {upstream}, behind {behind}"),
(Some(upstream), ahead, behind) => {
format!("tracking {upstream}, ahead {ahead}, behind {behind}")
}
(None, _, _) => "no upstream".to_owned(),
};
let cleanliness = if git.clean { "clean" } else { "dirty" };
format!("{} at {} ({tracking}, {cleanliness})", git.branch, git.head)
}
fn radar_git_changes(git: &GitState) -> Vec<String> {
let mut lines = Vec::new();
if !git.staged_files.is_empty() {
lines.push(format!("staged: {}", git.staged_files.join(", ")));
}
if !git.unstaged_files.is_empty() {
lines.push(format!("unstaged: {}", git.unstaged_files.join(", ")));
}
if !git.untracked_files.is_empty() {
lines.push(format!("untracked: {}", git.untracked_files.join(", ")));
}
lines
}
fn radar_material_findings(report: &RadarStateReport) -> Vec<String> {
let mut findings = Vec::new();
if let Some(anchor) = &report.execution_gates.attention_anchor {
findings.push(format!(
"Execution gate [{} #{}/{}]: {}",
anchor.status.as_str(),
anchor.index,
report.execution_gates.total_count,
anchor.text
));
} else if report.execution_gates.total_count > 0 {
if report.execution_gates.unfinished_count > 0 {
findings.push(format!(
"Execution gates: {} total, {} unfinished.",
report.execution_gates.total_count, report.execution_gates.unfinished_count
));
} else {
findings.push(format!(
"Execution gates: {} total.",
report.execution_gates.total_count
));
}
}
if let Some(note) = &report.repo_native_checks.surface.note {
findings.push(note.clone());
}
for check in &report.repo_native_checks.checks {
findings.push(format!(
"{} [{}]: {}",
check.id, check.status, check.message
));
}
if !matches!(
report.behavioral_drift.status,
DriftAggregateStatus::Aligned
) {
findings.push(report.behavioral_drift.summary.clone());
}
if report.work_stream_decay.status != "missing"
&& report.work_stream_decay.consecutive_no_progress > 0
{
findings.push(format!(
"Work-stream decay: {} consecutive no-progress attempt(s).",
report.work_stream_decay.consecutive_no_progress
));
}
findings
}
fn radar_cost_summary(cost: &telemetry_cost::TelemetryCostView) -> Option<String> {
let session_cost = cost.session_estimate_usd?;
let mut parts = vec![format!(
"session ${}",
crate::telemetry::cost::format_usd(session_cost)
)];
if let Some(model) = &cost.model {
parts.push(format!("model {model}"));
}
Some(parts.join(", "))
}
impl CommandReport for ContextCheckReport {
fn exit_code(&self) -> ExitCode {
ExitCode::SUCCESS
}
fn render_text(&self) {
println!(
"Context check: {} ({}, trigger {}, reason {})",
self.action.as_str(),
self.urgency.as_str(),
self.trigger.as_str(),
self.reason
);
println!("{}", self.payload.risk_summary);
if self.action == ContextCheckAction::InjectDelta {
println!("Policy digest: {}", self.payload.policy_digest);
println!("Focus digest: {}", self.payload.focus_digest);
println!("State delta: {}", self.payload.state_delta);
}
if self.throttle.suppressed {
println!(
"Throttle: suppressed; next interval hint {}s",
self.throttle.next_interval_hint_seconds
);
}
}
}
pub fn run(
repo_root: &Path,
explicit_profile: Option<&str>,
lightweight: bool,
) -> Result<RadarStateReport> {
let _span = info_span!("radar_state").entered();
Ok(build_radar_snapshot(repo_root, explicit_profile, lightweight)?.report)
}
pub fn run_with_digests(
repo_root: &Path,
explicit_profile: Option<&str>,
lightweight: bool,
) -> Result<(RadarStateReport, compiled_state::ProjectionDigests)> {
let _span = info_span!("radar_state").entered();
let snapshot = build_radar_snapshot(repo_root, explicit_profile, lightweight)?;
Ok((snapshot.report, snapshot.projection_digests))
}
fn build_radar_snapshot(
repo_root: &Path,
explicit_profile: Option<&str>,
lightweight: bool,
) -> Result<RadarSnapshot> {
let profile = profile::resolve(explicit_profile)?;
let profile_name = profile.to_string();
let layout = StateLayout::resolve(repo_root, profile.clone())?;
if !layout.profile_root().is_dir() {
bail!(
"profile `{}` does not exist at {}; bootstrap it with `ccd attach` before using `ccd radar-state`",
layout.profile(),
layout.profile_root().display()
);
}
let marker = repo_marker::load(repo_root)?.ok_or_else(|| {
anyhow::anyhow!(
"repo is not linked: {} is missing; bootstrap this clone with `ccd attach` or `ccd link` first",
repo_root.join(repo_marker::MARKER_FILE).display()
)
})?;
let git = if layout.resolved_substrate().is_git() {
Some(handoff::read_git_state(
repo_root,
BranchMode::AllowDetachedHead,
)?)
} else {
None
};
let checkout_state = git
.as_ref()
.map(|git| handoff::checkout_state_view(repo_root, git));
let runtime = runtime_state::load_runtime_state(repo_root, &layout, &marker.locality_id)?;
debug!("runtime state loaded");
let shared_db = db::StateDb::try_open_for_layout(&layout)?;
let (tracked_session, tracked_activity, active_session_id) = match shared_db.as_ref() {
Some(db) => (
session_state::load_from_db(Some(db))?,
session_state::load_activity_from_db(Some(db))?,
session_state::load_session_id_from_db(Some(db))?,
),
None => (
session_state::load_for_layout(&layout)?,
session_state::load_activity_for_layout(&layout)?,
session_state::load_session_id(&layout)?,
),
};
let work_stream_decay =
work_stream_decay::load_view_from_db(shared_db.as_ref(), active_session_id.as_deref())?;
let host_snapshot = host_telemetry::current(repo_root).ok().flatten();
let session_state_view =
build_session_state_view(&layout, tracked_session.as_ref(), tracked_activity.as_ref());
let loaded_handoff = read_handoff(
repo_root,
&layout,
&runtime,
git.as_ref(),
active_session_id.as_deref(),
&session_state_view,
)?;
let execution_gates = read_execution_gates(&runtime);
let effective_memory = read_effective_memory(&runtime);
let escalation_entries = if let Some(db) = shared_db.as_ref() {
escalation_state::load_from_db(Some(db))?
} else {
escalation_state::load_for_layout(&layout)?
};
let escalation_view = escalation_state::build_view(&layout, &escalation_entries);
let recovery = runtime_state::recovery_view(&runtime.recovery);
let (projection_telemetry, consistency_current_digests, projection_digests) =
build_projection_telemetry(
&layout,
shared_db.as_ref(),
&runtime,
&execution_gates,
&escalation_view,
&recovery,
&git,
&session_state_view,
active_session_id.as_deref(),
host_snapshot.as_ref(),
compiled_state::ProjectionTarget::Session,
)?;
let latest_projection_observation = if lightweight {
None
} else {
consistency::load_latest_projection_observation(&layout)?
};
let consistency_axes = if lightweight {
Vec::new()
} else {
consistency::evaluate_with_inputs(consistency::ConsistencyInputs {
repo_root,
layout: &layout,
locality_id: &marker.locality_id,
handoff: &runtime.state.handoff,
checkpoint: runtime.recovery.state.checkpoint.as_ref(),
include_checkpoint_references: false,
source_fingerprint: &projection_telemetry.source_fingerprint,
current_digests: &consistency_current_digests,
latest_observation: latest_projection_observation.as_ref(),
})?
.axes
};
let context_health = context_health::build_context_health(
&layout,
&marker.locality_id,
tracked_session.as_ref(),
active_session_id.as_deref(),
host_snapshot.as_ref(),
&runtime.state.handoff,
runtime.execution_gates.view.attention_anchor.as_ref(),
&work_stream_decay,
)?;
let behavioral_drift = behavioral_drift::build_behavioral_drift(
repo_root,
BehavioralDriftInputs {
layout: &layout,
locality_id: &marker.locality_id,
git: git.as_ref(),
handoff: &loaded_handoff.handoff,
handoff_contents: &loaded_handoff.contents,
runtime: &runtime,
runtime_handoff: &runtime.state.handoff,
tracked_session: tracked_session.as_ref(),
consistency_axes,
},
);
debug!("behavioral drift computed");
let (repo_native_checks, candidate_updates, evaluation, approval_steps) = if lightweight {
let repo_native_checks = RepoNativeChecksState::default_empty(repo_root);
let candidate_updates = CandidateUpdates::default_empty();
let evaluation = RadarEvaluation::default_empty();
let approval_steps = Vec::new();
(
repo_native_checks,
candidate_updates,
evaluation,
approval_steps,
)
} else {
let repo_native_checks = read_repo_native_checks(repo_root);
let branch_memory_available = runtime_state::resolve_active_branch_memory_path(
repo_root,
&layout,
&marker.locality_id,
)?
.is_some();
let candidate_updates = candidates::build_candidate_updates(
repo_root,
&profile_name,
&loaded_handoff.handoff,
&effective_memory,
tracked_session.as_ref(),
branch_memory_available,
);
let evaluation = evaluation::build_evaluation(EvaluationInputs {
git: git.as_ref(),
candidates: &candidate_updates,
execution_gates: &execution_gates,
effective_memory: &effective_memory,
repo_native_checks: &repo_native_checks,
behavioral_drift: &behavioral_drift,
escalation_view: &escalation_view,
});
let approval_steps = session_boundary::build_approval_steps(
&evaluation,
&candidate_updates,
&behavioral_drift,
);
(
repo_native_checks,
candidate_updates,
evaluation,
approval_steps,
)
};
let session_boundary = session_boundary::build_session_boundary(
checkout_state.as_ref(),
&recovery,
&work_stream_decay,
&context_health,
&behavioral_drift,
&evaluation,
&escalation_view,
);
debug!("radar evaluation complete");
let command_name = if lightweight {
"checkpoint"
} else {
"radar-state"
};
let mode_name = if lightweight { "checkpoint" } else { "radar" };
Ok(RadarSnapshot {
report: RadarStateReport {
command: command_name,
mode: mode_name,
schema_version: RADAR_STATE_SCHEMA_VERSION,
ok: true,
path: repo_root.display().to_string(),
profile: profile_name,
project_id: marker.locality_id.clone(),
locality_id: marker.locality_id,
handoff: loaded_handoff.handoff,
execution_gates,
effective_memory,
repo_native_checks,
git,
checkout_state,
session_state: session_state_view,
recovery,
escalation: escalation_view,
projection_telemetry,
work_stream_decay,
context_health,
session_boundary,
behavioral_drift,
evaluation,
candidate_updates,
approval_intents: vec![
"approve",
"approve_and_reuse_when_supported",
"reject_and_explain",
],
approval_steps,
},
policy_digest: context_check::build_policy_digest(&runtime),
projection_digests,
})
}
pub fn run_context_check(
repo_root: &Path,
explicit_profile: Option<&str>,
trigger: ContextCheckTrigger,
) -> Result<ContextCheckReport> {
let _span = info_span!("context_check").entered();
let snapshot = build_radar_snapshot(repo_root, explicit_profile, false)?;
let radar_report = snapshot.report;
let payload = ContextCheckPayload {
policy_digest: snapshot.policy_digest,
focus_digest: context_check::build_focus_digest(&radar_report),
state_delta: context_check::build_state_delta(&radar_report),
risk_summary: context_check::build_risk_summary(trigger, &radar_report),
};
let decision = context_check::build_context_check_decision(trigger, &radar_report, &payload);
Ok(ContextCheckReport {
command: "context-check",
schema_version: CONTEXT_CHECK_SCHEMA_VERSION,
ok: true,
path: radar_report.path,
profile: radar_report.profile,
project_id: radar_report.project_id,
locality_id: radar_report.locality_id,
trigger,
action: decision.action,
reason: decision.reason,
recommendation: decision.recommendation,
urgency: decision.urgency,
throttle: ContextCheckThrottle {
suppressed: decision.suppressed,
next_interval_hint_seconds: decision.next_interval_hint_seconds,
},
payload,
evidence: decision.evidence,
session_state: radar_report.session_state,
execution_gates: radar_report.execution_gates,
recovery: radar_report.recovery,
escalation: radar_report.escalation,
context_health: radar_report.context_health,
session_boundary: radar_report.session_boundary,
behavioral_drift: radar_report.behavioral_drift,
})
}
fn read_handoff(
repo_root: &Path,
layout: &StateLayout,
runtime: &runtime_state::LoadedRuntimeState,
git: Option<&GitState>,
active_session_id: Option<&str>,
session_view: &RadarSessionStateView,
) -> Result<LoadedHandoffState> {
let path = layout.state_db_path();
if runtime.sources.handoff.is_missing() {
let remediation = handoff_write_command(repo_root, layout);
let rerun = radar_state_command(repo_root, layout);
bail!(
"ccd radar-state cannot evaluate continuity or wrap-up readiness because the canonical workspace-local handoff is uninitialized at {}. Create it with `{remediation}` and rerun `{rerun}`.",
path.display(),
);
}
let mut current_system_state = handoff::current_system_state_lines(git, active_session_id);
if let Some(mode) = session_view.mode {
if session_view.status == "active" && mode != session_state::SessionMode::General {
current_system_state.push(format!("Session mode: `{}`", mode.as_str()));
}
}
let contents = runtime.sources.handoff.content.clone();
Ok(LoadedHandoffState {
handoff: HandoffState {
path: path.display().to_string(),
title: handoff::extract_title(&contents)?,
immediate_actions: handoff::extract_numbered_section(&contents, "Immediate Actions"),
current_system_state,
definition_of_done: handoff::extract_numbered_section(&contents, "Definition of Done"),
},
contents,
})
}
fn handoff_write_command(repo_root: &Path, layout: &StateLayout) -> String {
let base = format!("ccd handoff write --path {}", repo_root.display());
if layout.profile().as_str() == DEFAULT_PROFILE {
base
} else {
format!("{base} --profile {}", layout.profile().as_str())
}
}
fn radar_state_command(repo_root: &Path, layout: &StateLayout) -> String {
let base = format!("ccd radar-state --path {}", repo_root.display());
if layout.profile().as_str() == DEFAULT_PROFILE {
base
} else {
format!("{base} --profile {}", layout.profile().as_str())
}
}
pub(crate) fn build_session_state_view(
layout: &StateLayout,
tracked_session: Option<&session_state::SessionStateFile>,
tracked_activity: Option<&session_state::SessionActivityState>,
) -> RadarSessionStateView {
let path = layout.state_db_path().display().to_string();
match tracked_session {
Some(state) => {
let status = session_state_status(state, session_state::now_epoch_s().ok());
RadarSessionStateView {
path,
status,
session_id: state.session_id.clone(),
mode: Some(state.mode),
start_count: Some(state.start_count),
lifecycle: session_state::lifecycle_projection(
state,
session_state::now_epoch_s().unwrap_or_default(),
None,
tracked_activity,
),
}
}
None => RadarSessionStateView {
path,
status: "missing",
session_id: None,
mode: None,
start_count: None,
lifecycle: session_state::SessionLifecycleProjection::missing(),
},
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn build_projection_telemetry(
layout: &StateLayout,
shared_db: Option<&db::StateDb>,
runtime: &runtime_state::LoadedRuntimeState,
execution_gates: &session_gates::ExecutionGatesView,
escalation_view: &escalation_state::EscalationView,
recovery: &runtime_state::RuntimeRecoveryView,
git: &Option<GitState>,
session_state_view: &RadarSessionStateView,
active_session_id: Option<&str>,
host_snapshot: Option<&host_telemetry::HostContextSnapshot>,
target: compiled_state::ProjectionTarget,
) -> Result<(
ProjectionTelemetryView,
compiled_state::ProjectionDigests,
compiled_state::ProjectionDigests,
)> {
let source_fingerprint =
compiled_state::fingerprint_for_inputs(&runtime.sources, runtime.pod_identity_active)?;
let narrative = compiled_state::preview_with_precomputed_fingerprint(
layout,
shared_db,
runtime,
target,
&source_fingerprint,
true,
)?;
let symbolic = compiled_state::preview_symbolic_with_precomputed_fingerprint(
layout,
shared_db,
runtime,
target,
&source_fingerprint,
)?;
let bundle = compiled_state::preview_bundle_with_precomputed_fingerprint(
layout,
shared_db,
runtime,
target,
&source_fingerprint,
)?;
let execution_gates_json = serde_json::to_string(execution_gates).unwrap_or_default();
let escalation_json = serde_json::to_string(escalation_view).unwrap_or_default();
let recovery_json = serde_json::to_string(recovery).unwrap_or_default();
let git_state_json = serde_json::to_string(git).unwrap_or_default();
let session_state_json = serde_json::to_string(session_state_view).unwrap_or_default();
let base_digests = compiled_state::compute_projection_digests(&narrative.value);
let projection_digests = compiled_state::compute_extended_digests(
&base_digests,
&execution_gates_json,
&escalation_json,
&recovery_json,
&git_state_json,
&session_state_json,
);
let (delta_status, delta) = if let Some(session_id) = active_session_id {
let baseline_lookup = match shared_db {
Some(db) => projection_metadata::load_baseline_for_session_from_shared_db(
db, layout, session_id,
)?,
None => projection_metadata::load_baseline_for_session(layout, session_id)?,
};
match baseline_lookup {
Some(observation) => match observation.projection_digests {
Some(baseline) => (
"active_session",
compiled_state::compute_projection_delta_stats(&baseline, &projection_digests),
),
None => ("baseline_missing", default_projection_delta_stats()),
},
None => ("baseline_missing", default_projection_delta_stats()),
}
} else {
("no_active_session", default_projection_delta_stats())
};
let token_counts = compiled_state::ProjectionTokenTelemetry {
surface_tokens: compiled_state::compute_surface_token_counts(
&narrative.value,
&execution_gates_json,
&escalation_json,
&recovery_json,
&git_state_json,
&session_state_json,
),
format_tokens: compiled_state::compute_format_token_counts(
&narrative.value,
&symbolic.value,
&bundle.value,
),
};
let source_fingerprint = narrative.value.source_fingerprint.clone();
let mut current = BTreeMap::new();
current.insert(
compiled_state::ProjectionFormat::Narrative
.as_str()
.to_owned(),
narrative.cadence,
);
current.insert(
compiled_state::ProjectionFormat::Symbolic
.as_str()
.to_owned(),
symbolic.cadence,
);
current.insert(
compiled_state::ProjectionFormat::Bundle.as_str().to_owned(),
bundle.cadence,
);
Ok((
ProjectionTelemetryView {
source_fingerprint,
delta_status,
delta,
token_counts,
cadence: ProjectionCadenceView {
target: target.as_str(),
current,
history: compiled_state::cadence_summary_for_target(layout, target, 128)?,
provider_cache_usage: provider_cache_usage_view(host_snapshot),
},
},
base_digests,
projection_digests,
))
}
fn default_projection_delta_stats() -> compiled_state::ProjectionDeltaStats {
compiled_state::ProjectionDeltaStats {
skipped: 0,
refreshed: compiled_state::SESSION_RENDER_SURFACE_ORDER.len() as u32,
total: compiled_state::SESSION_RENDER_SURFACE_ORDER.len() as u32,
}
}
fn provider_cache_usage_view(
host_snapshot: Option<&host_telemetry::HostContextSnapshot>,
) -> Option<ProjectionProviderCacheUsageView> {
let usage = host_snapshot.map(|snapshot| &snapshot.cost_usage)?;
if usage.is_empty() {
return None;
}
Some(ProjectionProviderCacheUsageView {
cache_creation_input_tokens: (usage.cache_creation_input_tokens > 0)
.then_some(usage.cache_creation_input_tokens),
cache_read_input_tokens: (usage.cache_read_input_tokens > 0)
.then_some(usage.cache_read_input_tokens),
blended_total_tokens: usage.blended_total_tokens,
})
}
fn session_state_status(
state: &session_state::SessionStateFile,
now_epoch_s: Option<u64>,
) -> &'static str {
if state.session_id.is_none() {
return "stale";
}
match now_epoch_s {
Some(now) if session_state::is_stale(state, now) => "stale",
_ => "active",
}
}
fn read_execution_gates(
runtime: &runtime_state::LoadedRuntimeState,
) -> session_gates::ExecutionGatesView {
runtime.execution_gates.view.clone()
}
fn read_effective_memory(runtime: &runtime_state::LoadedRuntimeState) -> MemoryState {
let structured = memory_entries::inspect_sources_with_branch_and_clone(
some_if_loaded(&runtime.sources.profile_memory),
some_if_loaded(&runtime.sources.locality_memory),
some_if_loaded(&runtime.sources.pod_memory),
some_if_loaded(&runtime.sources.branch_memory),
some_if_loaded(&runtime.sources.clone_memory),
);
let mut segments = Vec::new();
let mut contributing_paths = Vec::new();
if let Some(contents) = some_if_loaded(&runtime.sources.profile_memory) {
if !contents.is_empty() {
segments.push(contents.to_owned());
contributing_paths.push(runtime.sources.profile_memory.path.display().to_string());
}
}
if let Some(contents) = some_if_loaded(&runtime.sources.locality_memory) {
if !contents.is_empty() {
segments.push(contents.to_owned());
contributing_paths.push(runtime.sources.locality_memory.path.display().to_string());
}
}
if let Some(contents) = some_if_loaded(&runtime.sources.pod_memory) {
if !contents.is_empty() {
segments.push(contents.to_owned());
contributing_paths.push(runtime.sources.pod_memory.path.display().to_string());
}
}
if let Some(contents) = some_if_loaded(&runtime.sources.branch_memory) {
if !contents.is_empty() {
segments.push(contents.to_owned());
contributing_paths.push(runtime.sources.branch_memory.path.display().to_string());
}
}
if let Some(contents) = some_if_loaded(&runtime.sources.clone_memory) {
if !contents.is_empty() {
segments.push(contents.to_owned());
contributing_paths.push(runtime.sources.clone_memory.path.display().to_string());
}
}
let content = segments.join("\n\n");
let status = if content.is_empty() {
"empty"
} else {
"loaded"
};
MemoryState {
status,
profile_path: runtime.sources.profile_memory.path.display().to_string(),
project_path: runtime.sources.locality_memory.path.display().to_string(),
locality_path: runtime.sources.locality_memory.path.display().to_string(),
pod_path: runtime.sources.pod_memory.path.display().to_string(),
work_stream_path: runtime.sources.branch_memory.path.display().to_string(),
branch_path: runtime.sources.branch_memory.path.display().to_string(),
workspace_path: runtime.sources.clone_memory.path.display().to_string(),
clone_path: runtime.sources.clone_memory.path.display().to_string(),
content,
structured,
contributing_paths,
}
}
fn read_repo_native_checks(repo_root: &Path) -> RepoNativeChecksState {
match check::run(repo_root) {
Ok(report) => RepoNativeChecksState {
surface: report.surface,
failures: report.failures,
checks: report
.checks
.into_iter()
.map(|check| RepoNativeCheckResult {
id: check.id,
path: check.path,
status: check.status,
severity: check.severity,
message: check.message,
duration_ms: check.duration_ms,
exit_code: check.exit_code,
})
.collect(),
},
Err(error) => {
let surface_path = repo_root.join(check::COMMANDS_DIR);
let message = format!("Failed to inspect repo-native checks: {error:#}");
RepoNativeChecksState {
surface: check::CheckSurfaceView {
path: surface_path.display().to_string(),
status: "error",
prefix: check::CHECK_PREFIX,
entries: Vec::new(),
note: Some(message.clone()),
},
failures: 1,
checks: vec![RepoNativeCheckResult {
id: "surface".to_owned(),
path: surface_path.display().to_string(),
status: "fail",
severity: "error",
message,
duration_ms: 0,
exit_code: None,
}],
}
}
}
}
fn some_if_loaded(surface: &runtime_state::RuntimeTextSurface) -> Option<&str> {
if surface.is_missing() {
None
} else {
Some(&surface.content)
}
}
fn active_session_start_count(
tracked_session: Option<&session_state::SessionStateFile>,
) -> Option<u64> {
let tracked_session = tracked_session?;
if session_state_status(tracked_session, session_state::now_epoch_s().ok()) != "active" {
return None;
}
Some(u64::from(tracked_session.start_count))
}
pub(crate) fn apply_delta_filter(
mut value: serde_json::Value,
baseline: &compiled_state::ProjectionDigests,
current: &compiled_state::ProjectionDigests,
) -> serde_json::Value {
let pairs: &[(&str, &str, &str)] = &[
("handoff", &baseline.handoff, ¤t.handoff),
(
"effective_memory",
&baseline.effective_memory,
¤t.effective_memory,
),
(
"execution_gates",
&baseline.execution_gates,
¤t.execution_gates,
),
("escalation", &baseline.escalation, ¤t.escalation),
("recovery", &baseline.recovery, ¤t.recovery),
("git", &baseline.git_state, ¤t.git_state),
(
"session_state",
&baseline.session_state,
¤t.session_state,
),
];
if let Some(obj) = value.as_object_mut() {
for &(field, base_digest, curr_digest) in pairs {
if !base_digest.is_empty() && base_digest == curr_digest {
let is_present_and_non_null = obj.get(field).is_some_and(|v| !v.is_null());
if is_present_and_non_null {
obj.insert(
field.to_owned(),
serde_json::json!({
"status": "unchanged",
"digest": curr_digest
}),
);
}
}
}
}
value
}
#[derive(Deserialize)]
pub(crate) struct CommitPayload {
#[serde(default)]
pub(crate) handoff: Option<crate::handoff::write::HandoffWriteInput>,
#[serde(default)]
pub(crate) recovery: Option<crate::recovery::RecoveryWriteInput>,
#[serde(default)]
pub(crate) clear_session: bool,
}
#[derive(Serialize)]
pub(crate) struct CommitResult {
pub(crate) ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) handoff: Option<CommitWriteOutcome>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) recovery: Option<CommitWriteOutcome>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) session_cleared: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) error: Option<String>,
}
#[derive(Serialize)]
pub(crate) struct CommitWriteOutcome {
pub(crate) ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) error: Option<String>,
}
pub(crate) fn commit_writes(
repo_root: &Path,
explicit_profile: Option<&str>,
payload: CommitPayload,
) -> CommitResult {
let write_options = crate::state::protected_write::ExclusiveWriteOptions::default();
let mut result = CommitResult {
ok: true,
handoff: None,
recovery: None,
session_cleared: None,
error: None,
};
if let Some(handoff_input) = payload.handoff {
match crate::handoff::write::run_with_input(
repo_root,
explicit_profile,
write_options.clone(),
handoff_input,
crate::handoff::write::WriteOptions::default(),
) {
Ok(_) => {
result.handoff = Some(CommitWriteOutcome {
ok: true,
error: None,
});
}
Err(e) => {
result.ok = false;
result.handoff = Some(CommitWriteOutcome {
ok: false,
error: Some(format!("{e:#}")),
});
result.error = Some(format!("handoff write failed: {e:#}"));
return result;
}
}
}
if let Some(recovery_input) = payload.recovery {
match crate::recovery::write_with_input(
repo_root,
explicit_profile,
recovery_input,
write_options,
) {
Ok(_) => {
result.recovery = Some(CommitWriteOutcome {
ok: true,
error: None,
});
}
Err(e) => {
result.ok = false;
result.recovery = Some(CommitWriteOutcome {
ok: false,
error: Some(format!("{e:#}")),
});
result.error = Some(format!("recovery write failed: {e:#}"));
return result;
}
}
}
if payload.clear_session {
let locality_id = crate::repo::marker::load(repo_root)
.ok()
.flatten()
.map(|m| m.locality_id);
match crate::state::session::clear(
repo_root,
explicit_profile,
locality_id.as_deref(),
crate::state::session::SessionClearOptions::default(),
) {
Ok(_) => {
result.session_cleared = Some(true);
}
Err(e) => {
result.ok = false;
result.session_cleared = Some(false);
result.error = Some(format!("session clear failed: {e:#}"));
return result;
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::behavioral_drift::{build_handoff_issue_references_signal, collect_session_commits};
use super::{session_state_status, DriftSignalStatus};
use crate::state::runtime::{RuntimeHandoffItem, RuntimeHandoffState, RuntimeLifecycle};
use crate::state::session::{SessionMode, SessionOwnerKind, SessionStateFile};
use std::path::Path;
#[test]
fn collect_session_commits_returns_err_on_non_git_path() {
let result = collect_session_commits(Path::new("/nonexistent-path-for-test"), 1);
assert!(result.is_err(), "expected Err for non-git path");
}
#[test]
fn handoff_issue_references_signal_fails_closed_on_git_log_failure() {
let handoff = RuntimeHandoffState {
title: "Next: continue ccd#999".to_owned(),
immediate_actions: vec![RuntimeHandoffItem {
text: "Work on #999.".to_owned(),
lifecycle: RuntimeLifecycle::Active,
}],
completed_state: Vec::new(),
operational_guardrails: Vec::new(),
key_files: Vec::new(),
definition_of_done: Vec::new(),
};
let session = SessionStateFile {
schema_version: 3,
started_at_epoch_s: 1,
last_started_at_epoch_s: 1,
start_count: 1,
session_id: Some("ses_test".to_owned()),
mode: SessionMode::General,
owner_kind: SessionOwnerKind::Interactive,
owner_id: None,
supervisor_id: None,
lease_ttl_secs: None,
last_heartbeat_at_epoch_s: None,
revision: 1,
};
let signal = build_handoff_issue_references_signal(
Path::new("/nonexistent-path-for-test"),
&handoff,
Some(&session),
);
assert!(
signal.status == DriftSignalStatus::Drift,
"git-log read failure with handoff issue refs present must fail closed; got summary={}",
signal.summary
);
assert!(
signal.evidence.iter().any(|line| line.contains("#999")),
"evidence must surface the unverified issue tokens, got: {:?}",
signal.evidence
);
}
#[test]
fn session_state_status_treats_missing_session_id_as_stale_without_clock() {
let state = SessionStateFile {
schema_version: 3,
started_at_epoch_s: 10,
last_started_at_epoch_s: 10,
start_count: 1,
session_id: None,
mode: SessionMode::Research,
owner_kind: SessionOwnerKind::Interactive,
owner_id: None,
supervisor_id: None,
lease_ttl_secs: None,
last_heartbeat_at_epoch_s: None,
revision: 0,
};
assert_eq!(session_state_status(&state, None), "stale");
}
}