use std::collections::BTreeSet;
use std::path::Path;
use std::process::ExitCode;
use anyhow::{bail, Result};
use clap::{ArgMatches, Command as ClapCommand};
use serde_json::Value;
use crate::commands::describe::CommandDescriptor;
use crate::handoff::GitState;
use crate::mcp::protocol::Tool;
use crate::output::OutputFormat;
use crate::paths::state::StateLayout;
#[cfg(any(feature = "extension-backlog", test))]
pub(crate) mod adapter;
#[cfg(feature = "extension-backlog")]
pub(crate) mod backlog;
#[cfg(feature = "extension-backlog")]
pub(crate) mod backlog_commands;
#[cfg(any(feature = "extension-backlog", test))]
pub(crate) mod backlog_config;
pub(crate) mod backlog_state;
#[cfg(feature = "extension-codemap")]
pub(crate) mod codemap;
#[cfg(feature = "extension-codemap")]
pub(crate) mod codemap_commands;
pub(crate) mod dispatch;
#[cfg_attr(not(feature = "extension-backlog"), allow(dead_code))]
pub(crate) struct HealthDiagnostic {
pub check: &'static str,
pub severity: &'static str,
pub file: String,
pub message: String,
pub details: Option<serde_json::Value>,
}
pub(crate) struct RadarEvaluationBucket {
pub status: &'static str,
pub summary: String,
pub evidence: Vec<String>,
}
pub(crate) struct RadarApprovalStep {
pub id: &'static str,
pub question: &'static str,
pub recommended_answer: &'static str,
pub recommendation: String,
pub evidence: Vec<String>,
}
pub(crate) struct RadarWorkflowGuidance {
pub evaluation: RadarEvaluationBucket,
pub approval_steps: Vec<RadarApprovalStep>,
}
#[cfg_attr(not(feature = "extension-backlog"), allow(dead_code))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RadarBehavioralDriftStatus {
Aligned,
Drift,
NoSignal,
}
#[cfg_attr(not(feature = "extension-backlog"), allow(dead_code))]
pub(crate) struct RadarBehavioralDriftSignal {
pub id: &'static str,
pub status: RadarBehavioralDriftStatus,
pub summary: String,
pub evidence: Vec<String>,
pub recommended_correction: Option<String>,
}
#[cfg_attr(not(feature = "extension-backlog"), allow(dead_code))]
pub(crate) struct StartupContext<'a> {
pub(crate) layout: &'a StateLayout,
pub(crate) repo_root: &'a Path,
pub(crate) locality_id: &'a str,
pub(crate) allow_cached_work: bool,
pub(crate) raw_backlog_cache: Option<&'a backlog_state::GitHubBacklogCache>,
}
#[cfg_attr(not(feature = "extension-backlog"), allow(dead_code))]
pub(crate) struct RadarContext<'a> {
pub(crate) startup: StartupContext<'a>,
pub(crate) git: Option<&'a GitState>,
pub(crate) handoff_title: &'a str,
pub(crate) handoff_immediate_actions: &'a [String],
pub(crate) active_session_id: Option<&'a str>,
}
pub(crate) trait Extension {
fn name(&self) -> &'static str;
fn command_groups(&self) -> &'static [&'static str];
fn cli_command(&self) -> Option<ClapCommand> {
None
}
fn dispatch_cli(
&self,
_subcommand_name: &str,
_matches: &ArgMatches,
_output: OutputFormat,
) -> Option<Result<ExitCode>> {
None
}
fn mcp_tools(&self, _commands: &[CommandDescriptor]) -> Vec<Tool> {
Vec::new()
}
fn dispatch_mcp(&self, _tool_name: &str, _args: &Value) -> Option<Result<Value>> {
None
}
fn work_queue_refresh_hint(
&self,
_provider: Option<&str>,
_repo: Option<&str>,
) -> Option<String> {
None
}
fn refresh_work_queue_cache(
&self,
_repo_root: &Path,
_layout: &StateLayout,
) -> Option<Result<(String, usize)>> {
None
}
fn load_work_queue_snapshot(
&self,
_layout: &StateLayout,
) -> Result<Option<backlog_state::GitHubBacklogCache>> {
Ok(None)
}
fn health_diagnostics(
&self,
_layout: &StateLayout,
_repo_root: &Path,
_locality_id: &str,
) -> Result<Vec<HealthDiagnostic>> {
Ok(Vec::new())
}
fn radar_workflow_guidance(
&self,
_ctx: &StartupContext<'_>,
) -> Result<Option<RadarWorkflowGuidance>> {
Ok(None)
}
fn radar_behavioral_drift_signals(
&self,
_ctx: &RadarContext<'_>,
) -> Result<Vec<RadarBehavioralDriftSignal>> {
Ok(Vec::new())
}
fn owns_dispatch(&self) -> bool {
false
}
fn observe_next_step(
&self,
_ctx: &StartupContext<'_>,
) -> Result<Option<dispatch::NextStepObservation>> {
Ok(None)
}
fn ensure_assignment(
&self,
_ctx: &StartupContext<'_>,
_owner: dispatch::AssignmentOwner<'_>,
) -> Result<dispatch::AssignmentOutcome> {
Ok(dispatch::AssignmentOutcome {
status: dispatch::AssignmentStatus::Skipped,
reason: None,
next_step: None,
assignment: None,
})
}
fn load_session_assignment(
&self,
_ctx: &StartupContext<'_>,
_session_id: &str,
) -> Result<Option<dispatch::AssignmentView>> {
Ok(None)
}
fn load_branch_assignment(
&self,
_ctx: &StartupContext<'_>,
_branch: &str,
) -> Result<Option<dispatch::AssignmentView>> {
Ok(None)
}
fn enrich_pod_status(
&self,
_pod_name: &str,
_locality_id: &str,
_profile: &str,
_shared_root: &Path,
) -> Option<Vec<(String, String)>> {
None
}
fn on_session_started(
&self,
_ctx: &dispatch::SessionBoundaryContext<'_>,
) -> Result<dispatch::SessionBoundaryEffect> {
Ok(dispatch::SessionBoundaryEffect::empty())
}
fn on_session_cleared(
&self,
_ctx: &dispatch::SessionBoundaryContext<'_>,
) -> Result<dispatch::SessionBoundaryEffect> {
Ok(dispatch::SessionBoundaryEffect::empty())
}
fn resolve_assignment_references(
&self,
_ctx: &StartupContext<'_>,
_assignment: &dispatch::AssignmentView,
_cache: Option<&backlog_state::GitHubBacklogCache>,
) -> Result<Vec<dispatch::StartupAlert>> {
Ok(Vec::new())
}
}
#[cfg(all(feature = "extension-backlog", feature = "extension-codemap"))]
pub(crate) fn registered() -> Vec<&'static dyn Extension> {
vec![&backlog::BACKLOG_EXTENSION, &codemap::CODEMAP_EXTENSION]
}
#[cfg(all(feature = "extension-backlog", not(feature = "extension-codemap")))]
pub(crate) fn registered() -> Vec<&'static dyn Extension> {
vec![&backlog::BACKLOG_EXTENSION]
}
#[cfg(all(not(feature = "extension-backlog"), feature = "extension-codemap"))]
pub(crate) fn registered() -> Vec<&'static dyn Extension> {
vec![&codemap::CODEMAP_EXTENSION]
}
#[cfg(all(not(feature = "extension-backlog"), not(feature = "extension-codemap")))]
pub(crate) fn registered() -> Vec<&'static dyn Extension> {
Vec::new()
}
#[cfg(test)]
pub(crate) fn owned_command_groups() -> Vec<&'static str> {
registered()
.into_iter()
.flat_map(|extension| extension.command_groups().iter().copied())
.collect()
}
pub(crate) fn augment_clap(mut command: ClapCommand) -> ClapCommand {
for extension in registered() {
debug_assert!(!extension.name().is_empty());
debug_assert!(!extension.command_groups().is_empty());
if let Some(subcommand) = extension.cli_command() {
command = command.subcommand(subcommand);
}
}
command
}
pub(crate) fn dispatch_cli(
subcommand_name: &str,
matches: &ArgMatches,
output: OutputFormat,
) -> Option<Result<ExitCode>> {
for extension in registered() {
if let Some(result) = extension.dispatch_cli(subcommand_name, matches, output) {
return Some(result);
}
}
None
}
pub(crate) fn build_mcp_tools(commands: &[CommandDescriptor]) -> Vec<Tool> {
let mut tools = Vec::new();
for extension in registered() {
debug_assert!(!extension.name().is_empty());
debug_assert!(!extension.command_groups().is_empty());
tools.extend(extension.mcp_tools(commands));
}
tools
}
pub(crate) fn dispatch_mcp(tool_name: &str, args: &Value) -> Option<Result<Value>> {
for extension in registered() {
if let Some(report) = extension.dispatch_mcp(tool_name, args) {
return Some(report);
}
}
None
}
pub(crate) fn work_queue_refresh_hint(
provider: Option<&str>,
repo: Option<&str>,
) -> Option<String> {
for extension in registered() {
if let Some(hint) = extension.work_queue_refresh_hint(provider, repo) {
return Some(hint);
}
}
None
}
pub(crate) fn refresh_work_queue_cache(
repo_root: &Path,
layout: &StateLayout,
) -> Option<Result<(String, usize)>> {
for extension in registered() {
if let Some(result) = extension.refresh_work_queue_cache(repo_root, layout) {
return Some(result);
}
}
None
}
pub(crate) fn load_work_queue_snapshot(
layout: &StateLayout,
) -> Result<Option<backlog_state::GitHubBacklogCache>> {
for extension in registered() {
if let Some(snapshot) = extension.load_work_queue_snapshot(layout)? {
return Ok(Some(snapshot));
}
}
Ok(None)
}
pub(crate) fn health_diagnostics(
layout: &StateLayout,
repo_root: &Path,
locality_id: &str,
) -> Result<Vec<HealthDiagnostic>> {
let mut all = Vec::new();
for extension in registered() {
all.extend(extension.health_diagnostics(layout, repo_root, locality_id)?);
}
Ok(all)
}
pub(crate) fn radar_workflow_guidance(
layout: &StateLayout,
repo_root: &Path,
locality_id: &str,
allow_cached_work: bool,
) -> Result<Option<RadarWorkflowGuidance>> {
let raw_backlog_cache = load_work_queue_snapshot(layout)?;
let ctx = StartupContext {
layout,
repo_root,
locality_id,
allow_cached_work,
raw_backlog_cache: raw_backlog_cache.as_ref(),
};
collect_radar_workflow_guidance_from(®istered(), &ctx)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn radar_behavioral_drift_signals(
layout: &StateLayout,
repo_root: &Path,
locality_id: &str,
allow_cached_work: bool,
git: Option<&GitState>,
handoff_title: &str,
handoff_immediate_actions: &[String],
active_session_id: Option<&str>,
) -> Result<Vec<RadarBehavioralDriftSignal>> {
let raw_backlog_cache = load_work_queue_snapshot(layout)?;
let ctx = RadarContext {
startup: StartupContext {
layout,
repo_root,
locality_id,
allow_cached_work,
raw_backlog_cache: raw_backlog_cache.as_ref(),
},
git,
handoff_title,
handoff_immediate_actions,
active_session_id,
};
collect_radar_behavioral_drift_signals_from(®istered(), &ctx)
}
fn collect_radar_workflow_guidance_from(
extensions: &[&dyn Extension],
ctx: &StartupContext<'_>,
) -> Result<Option<RadarWorkflowGuidance>> {
let mut guidance = None;
let mut owner = None;
for extension in extensions {
if let Some(candidate) = extension.radar_workflow_guidance(ctx)? {
if let Some(existing_owner) = owner {
bail!(
"multiple extensions (`{existing_owner}` and `{}`) contributed radar workflow guidance; exactly one guidance contributor is supported",
extension.name()
);
}
owner = Some(extension.name());
guidance = Some(candidate);
}
}
Ok(guidance)
}
fn collect_radar_behavioral_drift_signals_from(
extensions: &[&dyn Extension],
ctx: &RadarContext<'_>,
) -> Result<Vec<RadarBehavioralDriftSignal>> {
let mut seen = BTreeSet::new();
let mut signals = Vec::new();
for extension in extensions {
for signal in extension.radar_behavioral_drift_signals(ctx)? {
if !seen.insert(signal.id) {
bail!(
"multiple extensions contributed radar behavioral drift signal `{}`; signal ids must be unique",
signal.id
);
}
signals.push(signal);
}
}
Ok(signals)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profile::ProfileName;
struct TestExtension {
name: &'static str,
workflow_guidance: Option<&'static str>,
behavioral_drift_signal: Option<&'static str>,
}
impl Extension for TestExtension {
fn name(&self) -> &'static str {
self.name
}
fn command_groups(&self) -> &'static [&'static str] {
&["test"]
}
fn radar_workflow_guidance(
&self,
_ctx: &StartupContext<'_>,
) -> Result<Option<RadarWorkflowGuidance>> {
Ok(self.workflow_guidance.map(|summary| RadarWorkflowGuidance {
evaluation: RadarEvaluationBucket {
status: "loaded",
summary: summary.to_owned(),
evidence: Vec::new(),
},
approval_steps: Vec::new(),
}))
}
fn radar_behavioral_drift_signals(
&self,
_ctx: &RadarContext<'_>,
) -> Result<Vec<RadarBehavioralDriftSignal>> {
Ok(self
.behavioral_drift_signal
.map(|id| {
vec![RadarBehavioralDriftSignal {
id,
status: RadarBehavioralDriftStatus::NoSignal,
summary: "test".to_owned(),
evidence: Vec::new(),
recommended_correction: None,
}]
})
.unwrap_or_default())
}
}
#[test]
fn owned_command_groups_reflect_registered_extensions() {
let groups = owned_command_groups();
#[cfg(feature = "extension-backlog")]
assert!(groups.contains(&"backlog"));
#[cfg(not(feature = "extension-backlog"))]
assert!(!groups.contains(&"backlog"));
#[cfg(feature = "extension-codemap")]
assert!(groups.contains(&"codemap"));
#[cfg(not(feature = "extension-codemap"))]
assert!(!groups.contains(&"codemap"));
}
#[test]
fn extension_mcp_tools_reflect_registered_extensions() {
let schema = crate::commands::describe::run();
let tools = build_mcp_tools(&schema.commands);
let names = tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>();
#[cfg(feature = "extension-backlog")]
assert!(names.contains(&"ccd_backlog"));
#[cfg(not(feature = "extension-backlog"))]
assert!(!names.contains(&"ccd_backlog"));
#[cfg(feature = "extension-codemap")]
assert!(names.contains(&"ccd_codemap"));
#[cfg(not(feature = "extension-codemap"))]
assert!(!names.contains(&"ccd_codemap"));
}
#[test]
fn extension_cli_commands_reflect_registered_extensions() {
let command = augment_clap(clap::Command::new("ccd"));
let names = command
.get_subcommands()
.map(|command| command.get_name())
.collect::<Vec<_>>();
#[cfg(feature = "extension-backlog")]
assert!(names.contains(&"backlog"));
#[cfg(not(feature = "extension-backlog"))]
assert!(!names.contains(&"backlog"));
#[cfg(feature = "extension-codemap")]
assert!(names.contains(&"codemap"));
#[cfg(not(feature = "extension-codemap"))]
assert!(!names.contains(&"codemap"));
}
#[test]
fn radar_workflow_guidance_is_not_limited_to_dispatch_owner() {
let extension = TestExtension {
name: "observer",
workflow_guidance: Some("generic workflow guidance"),
behavioral_drift_signal: None,
};
let extensions: [&dyn Extension; 1] = [&extension];
let layout = StateLayout::new(
"/tmp/home/.ccd".into(),
"/tmp/repo/.git/ccd".into(),
ProfileName::new("main").expect("profile"),
);
let ctx = StartupContext {
layout: &layout,
repo_root: Path::new("/tmp/repo"),
locality_id: "ccdrepo_test",
allow_cached_work: true,
raw_backlog_cache: None,
};
let guidance =
collect_radar_workflow_guidance_from(&extensions, &ctx).expect("workflow guidance");
assert_eq!(
guidance.expect("guidance present").evaluation.summary,
"generic workflow guidance"
);
}
#[test]
fn radar_behavioral_drift_signals_are_not_limited_to_dispatch_owner() {
let extension = TestExtension {
name: "observer",
workflow_guidance: None,
behavioral_drift_signal: Some("observer_signal"),
};
let extensions: [&dyn Extension; 1] = [&extension];
let layout = StateLayout::new(
"/tmp/home/.ccd".into(),
"/tmp/repo/.git/ccd".into(),
ProfileName::new("main").expect("profile"),
);
let startup = StartupContext {
layout: &layout,
repo_root: Path::new("/tmp/repo"),
locality_id: "ccdrepo_test",
allow_cached_work: true,
raw_backlog_cache: None,
};
let git = GitState {
branch: "main".to_owned(),
head: "deadbee".to_owned(),
upstream: None,
ahead: 0,
behind: 0,
clean: true,
staged_files: Vec::new(),
unstaged_files: Vec::new(),
untracked_files: Vec::new(),
recent_commits: Vec::new(),
};
let ctx = RadarContext {
startup,
git: Some(&git),
handoff_title: "Next Session: Test",
handoff_immediate_actions: &[],
active_session_id: None,
};
let signals = collect_radar_behavioral_drift_signals_from(&extensions, &ctx)
.expect("behavioral drift signals");
assert_eq!(signals.len(), 1);
assert_eq!(signals[0].id, "observer_signal");
assert_eq!(signals[0].status, RadarBehavioralDriftStatus::NoSignal);
}
}