use std::path::{Component, Path, PathBuf};
use std::process::ExitCode;
use anyhow::Result;
use clap::{ArgMatches, Command as ClapCommand};
use serde_json::Value;
use crate::commands::describe::CommandDescriptor;
#[cfg(feature = "extension-backlog")]
use crate::content_trust::ContentTrust;
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;
#[cfg(any(feature = "extension-backlog", test))]
mod backlog_state;
#[cfg(feature = "extension-codemap")]
pub(crate) mod codemap;
#[cfg(feature = "extension-codemap")]
pub(crate) mod codemap_commands;
#[cfg(feature = "extension-backlog")]
mod dispatch;
#[cfg(any(feature = "extension-backlog", test))]
mod dispatch_state;
pub(crate) mod issue_refs;
pub(crate) mod work_queue;
#[cfg_attr(not(feature = "extension-backlog"), allow(unused_imports))]
pub(crate) use work_queue::{
BacklogRef, WorkQueueCacheView, WorkQueueSnapshot, WorkQueueSnapshotItem, WorkQueueSummaryItem,
};
pub(crate) fn migrate_repo_overlay_config_if_needed(
layout: &StateLayout,
locality_id: &str,
) -> Result<Option<Vec<PathBuf>>> {
#[cfg(any(feature = "extension-backlog", test))]
{
backlog_config::migrate_repo_overlay_if_needed(layout, locality_id)
}
#[cfg(not(any(feature = "extension-backlog", test)))]
{
let _ = (layout, locality_id);
Ok(None)
}
}
pub(crate) fn should_transfer_repo_overlay_entry(relative_path: &Path) -> bool {
if relative_path == Path::new("dispatch-state.md") {
return false;
}
!matches!(
relative_path.components().next(),
Some(Component::Normal(name)) if name == std::ffi::OsStr::new("extensions")
)
}
#[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>,
}
#[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) 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 health_diagnostics(
&self,
_layout: &StateLayout,
_repo_root: &Path,
_locality_id: &str,
) -> Result<Vec<HealthDiagnostic>> {
Ok(Vec::new())
}
fn enrich_pod_status(
&self,
_pod_name: &str,
_locality_id: &str,
_profile: &str,
_shared_root: &Path,
) -> Option<Vec<(String, String)>> {
None
}
}
#[cfg(feature = "extension-backlog")]
pub(crate) trait WorkflowExtension: Extension {
fn load_work_queue_snapshot(&self, layout: &StateLayout) -> Result<Option<WorkQueueSnapshot>>;
fn observe_next_step(
&self,
ctx: &StartupContext<'_>,
) -> Result<Option<dispatch::NextStepObservation>>;
fn ensure_assignment(
&self,
ctx: &StartupContext<'_>,
owner: dispatch::AssignmentOwner<'_>,
) -> Result<dispatch::AssignmentOutcome>;
fn load_session_assignment(
&self,
ctx: &StartupContext<'_>,
session_id: &str,
) -> Result<Option<dispatch::AssignmentView>>;
fn load_branch_assignment(
&self,
ctx: &StartupContext<'_>,
branch: &str,
) -> Result<Option<dispatch::AssignmentView>>;
fn on_session_started(&self, ctx: &dispatch::SessionBoundaryContext<'_>) -> Result<()>;
fn on_session_cleared(&self, ctx: &dispatch::SessionBoundaryContext<'_>) -> Result<()>;
fn resolve_assignment_references(
&self,
ctx: &StartupContext<'_>,
assignment: &dispatch::AssignmentView,
) -> Result<Vec<dispatch::StartupAlert>>;
}
#[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(feature = "extension-backlog")]
pub(crate) fn registered_workflow() -> Vec<&'static dyn WorkflowExtension> {
vec![&backlog::BACKLOG_EXTENSION]
}
#[cfg(feature = "extension-backlog")]
pub(crate) fn workflow_command_groups() -> Vec<Vec<&'static str>> {
registered_workflow()
.into_iter()
.map(|extension| extension.command_groups().to_vec())
.collect()
}
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn workflow_command_groups() -> Vec<Vec<&'static str>> {
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
}
#[cfg(feature = "extension-backlog")]
pub(crate) fn load_work_queue_snapshot(layout: &StateLayout) -> Result<Option<WorkQueueSnapshot>> {
for extension in registered_workflow() {
if let Some(snapshot) = extension.load_work_queue_snapshot(layout)? {
return Ok(Some(snapshot));
}
}
Ok(None)
}
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn load_work_queue_snapshot(_layout: &StateLayout) -> Result<Option<WorkQueueSnapshot>> {
Ok(None)
}
pub(crate) fn load_work_queue_view(
layout: &StateLayout,
limit: usize,
) -> Result<WorkQueueCacheView> {
let snapshot = load_work_queue_snapshot(layout)?;
Ok(work_queue::cache_view_from_snapshot(
layout,
snapshot.as_ref(),
limit,
))
}
fn startup_dispatch_assignment_reason(
active_session_id: Option<&str>,
landed_trunk: Option<&str>,
branch: Option<&str>,
) -> Option<String> {
match (active_session_id, landed_trunk, branch) {
(Some(_), _, _) => None,
(None, Some(trunk), Some(branch)) => Some(format!(
"branch `{branch}` is already landed on local `origin/{trunk}`; \
local redispatch is skipped"
)),
(None, None, Some(branch)) if !branch.is_empty() => None,
_ => Some(
"directory substrate has no pre-session work stream identity; choose the next item explicitly or start a session first"
.to_owned(),
),
}
}
fn startup_dispatch_needs_input_reason(assignment_reason: Option<&str>) -> String {
assignment_reason.unwrap_or(
"no extension-owned next-step observation is available; choose the next item explicitly or use a neutral session name",
)
.to_owned()
}
#[cfg(feature = "extension-backlog")]
fn startup_dispatch_assignment_view(
assignment: dispatch_state::LocalAssignmentView,
) -> dispatch::AssignmentView {
let owner = if assignment.session_id.is_empty() {
dispatch::AssignmentOwnerView::PreSessionBranch {
branch: assignment.branch.clone().unwrap_or_default(),
}
} else {
dispatch::AssignmentOwnerView::Session {
session_id: assignment.session_id.clone(),
}
};
dispatch::AssignmentView {
backlog_ref: assignment.backlog_ref,
ccd_id: assignment.ccd_id,
github_issue_number: assignment.github_issue_number,
content_trust: ContentTrust::ExternalAdapterOutput,
title: assignment.title,
owner,
branch: assignment.branch,
worktree: assignment.worktree,
}
}
#[cfg(feature = "extension-backlog")]
fn resolve_startup_dispatch_assignment(
layout: &StateLayout,
locality_id: &str,
active_session_id: Option<&str>,
branch: Option<&str>,
) -> Result<Option<dispatch::AssignmentView>> {
if let Some(session_id) = active_session_id.filter(|session_id| !session_id.is_empty()) {
if let Some(assignment) =
dispatch_state::resolve_session_assignment(layout, locality_id, session_id, None)?
{
return Ok(Some(startup_dispatch_assignment_view(assignment)));
}
}
match branch {
Some(branch) if !branch.is_empty() && branch != "HEAD" => {
dispatch_state::resolve_branch_assignment(layout, locality_id, branch, None)
.map(|assignment| assignment.map(startup_dispatch_assignment_view))
}
_ => Ok(None),
}
}
#[cfg(feature = "extension-backlog")]
pub(crate) fn build_startup_dispatch_compat(
layout: &StateLayout,
locality_id: &str,
active_session_id: Option<&str>,
branch: Option<&str>,
landed_trunk: Option<&str>,
) -> Result<Value> {
let active_assignment =
resolve_startup_dispatch_assignment(layout, locality_id, active_session_id, branch)?;
let assignment_reason =
startup_dispatch_assignment_reason(active_session_id, landed_trunk, branch);
let next_step = if let Some(assignment) = active_assignment.clone() {
dispatch::ExtensionNextStepView {
status: dispatch::NextStepStatus::Observed,
source: dispatch::NextStepSource::ActiveAssignment,
reason: None,
observation: Some(dispatch::NextStepObservation {
item: dispatch::NextStepItem {
backlog_ref: assignment.backlog_ref.clone(),
ccd_id: assignment.ccd_id,
github_issue_number: assignment.github_issue_number,
content_trust: assignment.content_trust,
title: assignment.title.clone(),
branch: assignment.branch.clone(),
},
confidence: dispatch::NextStepConfidence::Unverified,
}),
}
} else {
dispatch::ExtensionNextStepView {
status: dispatch::NextStepStatus::NeedsInput,
source: dispatch::NextStepSource::ExplicitActorInput,
reason: Some(startup_dispatch_needs_input_reason(
assignment_reason.as_deref(),
)),
observation: None,
}
};
let assignment = match active_assignment {
Some(_assignment) if active_session_id.is_none() && landed_trunk.is_some() => {
dispatch::AssignmentOutcome {
status: dispatch::AssignmentStatus::Skipped,
reason: assignment_reason,
next_step: None,
assignment: None,
}
}
Some(assignment) => dispatch::AssignmentOutcome {
status: dispatch::AssignmentStatus::Existing,
reason: None,
next_step: None,
assignment: Some(assignment),
},
None => dispatch::AssignmentOutcome {
status: dispatch::AssignmentStatus::Skipped,
reason: assignment_reason,
next_step: None,
assignment: None,
},
};
Ok(serde_json::to_value(dispatch::ExtensionStartupPayload {
next_step,
assignment: Some(assignment),
})?)
}
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn build_startup_dispatch_compat(
_layout: &StateLayout,
_locality_id: &str,
active_session_id: Option<&str>,
branch: Option<&str>,
landed_trunk: Option<&str>,
) -> Result<Value> {
let assignment_reason =
startup_dispatch_assignment_reason(active_session_id, landed_trunk, branch);
let next_step_reason = startup_dispatch_needs_input_reason(assignment_reason.as_deref());
let mut next_step = serde_json::Map::new();
next_step.insert("status".to_owned(), Value::String("needs_input".to_owned()));
next_step.insert(
"source".to_owned(),
Value::String("explicit_actor_input".to_owned()),
);
next_step.insert("reason".to_owned(), Value::String(next_step_reason));
let mut assignment = serde_json::Map::new();
assignment.insert("status".to_owned(), Value::String("skipped".to_owned()));
if let Some(reason) = assignment_reason {
assignment.insert("reason".to_owned(), Value::String(reason));
}
let mut payload = serde_json::Map::new();
payload.insert("next_step".to_owned(), Value::Object(next_step));
payload.insert("assignment".to_owned(), Value::Object(assignment));
Ok(Value::Object(payload))
}
#[cfg_attr(not(test), allow(dead_code))]
#[allow(clippy::type_complexity)]
#[cfg(feature = "extension-backlog")]
pub(crate) fn prepare_startup_extension(
ctx: &StartupContext<'_>,
active_session_id: Option<&str>,
branch: Option<&str>,
_worktree: &str,
landed_trunk: Option<&str>,
) -> Result<(
Value,
Vec<(&'static str, &'static str, String)>,
bool,
Option<String>,
Option<String>,
)> {
let pre_assignment_snapshot = match active_session_id {
Some(session_id) => dispatch::load_session_assignment(ctx, session_id)?,
None => match branch {
Some(branch) if !branch.is_empty() => dispatch::load_branch_assignment(ctx, branch)?,
_ => None,
},
};
let assignment_outcome = match active_session_id {
Some(session_id) => dispatch::ensure_assignment(
ctx,
dispatch::AssignmentOwner::Session {
session_id,
branch: branch.and_then(|current| (current != "HEAD").then_some(current)),
},
)?,
None => match (landed_trunk, branch) {
(Some(trunk), Some(branch)) => dispatch::AssignmentOutcome {
status: dispatch::AssignmentStatus::Skipped,
reason: Some(format!(
"branch `{branch}` is already landed on local `origin/{trunk}`; \
local redispatch is skipped"
)),
next_step: None,
assignment: None,
},
(None, Some(branch)) if !branch.is_empty() => dispatch::ensure_assignment(
ctx,
dispatch::AssignmentOwner::PreSessionBranch { branch },
)?,
_ => dispatch::AssignmentOutcome {
status: dispatch::AssignmentStatus::Skipped,
reason: Some(
"directory substrate has no pre-session work stream identity; choose the next item explicitly or start a session first"
.to_owned(),
),
next_step: None,
assignment: None,
},
},
};
let startup_payload = dispatch::build_startup_payload(ctx, &assignment_outcome)?;
let drives_needs_input = assignment_outcome.assignment.is_some()
|| (startup_payload.next_step.status == dispatch::NextStepStatus::NeedsInput
&& startup_payload.next_step.source == dispatch::NextStepSource::BacklogAdapter);
let boundary_stop_reason = (startup_payload.next_step.status
== dispatch::NextStepStatus::NeedsInput)
.then(|| startup_payload.next_step.reason.clone())
.flatten();
let continuity_note = if let Some(observation) = &startup_payload.next_step.observation {
let observed_ref = if observation.item.ccd_id != 0 {
format!("ccd#{}", observation.item.ccd_id)
} else if observation.item.github_issue_number != 0 {
format!("GH#{}", observation.item.github_issue_number)
} else {
observation.item.title.clone()
};
Some(format!(
"Extension-owned next step is currently `{observed_ref}`."
))
} else if assignment_outcome.assignment.is_some() {
Some("A local assignment is already active for this session context.".to_owned())
} else {
None
};
let mut alerts = Vec::new();
let ref_check_assignment = assignment_outcome
.assignment
.as_ref()
.or(pre_assignment_snapshot.as_ref());
if let Some(assignment) = ref_check_assignment {
for alert in dispatch::resolve_assignment_references(ctx, assignment)? {
let severity = match alert.severity {
dispatch::StartupAlertSeverity::Warning => "warning",
};
alerts.push((alert.check, severity, alert.message));
}
}
if let (Some(current_branch), Some(session_id), Some(assignment)) = (
branch,
active_session_id,
assignment_outcome.assignment.as_ref(),
) {
if !session_id.is_empty() && current_branch != "HEAD" {
if let Some(recorded_branch) = assignment.branch.as_ref() {
if recorded_branch != current_branch {
alerts.push((
"session_branch_drift",
"warning",
format!(
"session assignment was created on branch `{recorded_branch}` \
but current branch is `{current_branch}`"
),
));
}
}
}
}
Ok((
serde_json::to_value(startup_payload)?,
alerts,
drives_needs_input,
boundary_stop_reason,
continuity_note,
))
}
#[cfg_attr(not(test), allow(dead_code))]
#[allow(clippy::type_complexity)]
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn prepare_startup_extension(
_ctx: &StartupContext<'_>,
active_session_id: Option<&str>,
branch: Option<&str>,
_worktree: &str,
landed_trunk: Option<&str>,
) -> Result<(
Value,
Vec<(&'static str, &'static str, String)>,
bool,
Option<String>,
Option<String>,
)> {
let assignment_reason = match (active_session_id, landed_trunk, branch) {
(Some(_), _, _) => None,
(None, Some(trunk), Some(branch)) => Some(format!(
"branch `{branch}` is already landed on local `origin/{trunk}`; \
local redispatch is skipped"
)),
(None, None, Some(branch)) if !branch.is_empty() => None,
_ => Some(
"directory substrate has no pre-session work stream identity; choose the next item explicitly or start a session first"
.to_owned(),
),
};
let next_step_reason = assignment_reason.clone().unwrap_or_else(|| {
"no extension-owned next-step observation is available; choose the next item explicitly or use a neutral session name"
.to_owned()
});
let mut next_step = serde_json::Map::new();
next_step.insert("status".to_owned(), Value::String("needs_input".to_owned()));
next_step.insert(
"source".to_owned(),
Value::String("explicit_actor_input".to_owned()),
);
next_step.insert("reason".to_owned(), Value::String(next_step_reason.clone()));
let mut assignment = serde_json::Map::new();
assignment.insert("status".to_owned(), Value::String("skipped".to_owned()));
if let Some(reason) = assignment_reason {
assignment.insert("reason".to_owned(), Value::String(reason));
}
let mut payload = serde_json::Map::new();
payload.insert("next_step".to_owned(), Value::Object(next_step));
payload.insert("assignment".to_owned(), Value::Object(assignment));
Ok((
Value::Object(payload),
Vec::new(),
false,
Some(next_step_reason),
None,
))
}
#[cfg(feature = "extension-backlog")]
pub(crate) fn on_session_started(
layout: &StateLayout,
_repo_root: &Path,
locality_id: &str,
session_id: &str,
_pod: Option<(&str, &Path)>,
) -> Result<()> {
let ctx = dispatch::SessionBoundaryContext {
layout,
locality_id,
session_id,
};
dispatch::on_session_started(&ctx)
}
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn on_session_started(
_layout: &StateLayout,
_repo_root: &Path,
_locality_id: &str,
_session_id: &str,
_pod: Option<(&str, &Path)>,
) -> Result<()> {
Ok(())
}
#[cfg(feature = "extension-backlog")]
pub(crate) fn on_session_cleared(
layout: &StateLayout,
_repo_root: &Path,
locality_id: &str,
session_id: &str,
_pod: Option<(&str, &Path)>,
) -> Result<()> {
let ctx = dispatch::SessionBoundaryContext {
layout,
locality_id,
session_id,
};
dispatch::on_session_cleared(&ctx)
}
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn on_session_cleared(
_layout: &StateLayout,
_repo_root: &Path,
_locality_id: &str,
_session_id: &str,
_pod: Option<(&str, &Path)>,
) -> Result<()> {
Ok(())
}
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)
}
#[cfg(test)]
mod tests {
use super::*;
#[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"));
}
}