use std::collections::HashSet;
use std::path::Path;
use std::process::ExitCode;
use anyhow::{bail, Result};
use serde::Serialize;
use crate::handoff::{self, BranchMode};
use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile;
use crate::repo::marker as repo_marker;
use crate::state::compiled::{self as compiled_state, BundleProjectionView, BundleSectionKind};
use crate::state::runtime as runtime_state;
use crate::state::session as session_state;
const SESSION_NAME_STOPWORDS: &[&str] = &[
"a", "an", "and", "for", "from", "in", "into", "of", "on", "or", "the", "to", "with",
];
const SESSION_NAME_GENERIC_TOKENS: &[&str] = &[
"ccd",
"child",
"current",
"delegated",
"delegation",
"next",
"session",
"start",
"task",
"work",
];
const PARENT_ONLY_COMMANDS: &[&str] = &[
"ccd session-state start",
"ccd session-state clear",
"ccd session-state takeover",
"ccd radar-state",
"ccd checkpoint",
"ccd handoff write",
"ccd recovery write",
"ccd session-state gates seed",
"ccd session-state gates replace",
"ccd session-state gates set-status",
"ccd session-state gates advance",
"ccd session-state gates clear",
];
#[derive(Serialize)]
pub struct RuntimeStateChildBootstrapReport {
command: &'static str,
ok: bool,
path: String,
profile: String,
project_id: String,
locality_id: String,
child_bootstrap: DelegatedChildBootstrapView,
}
#[derive(Serialize)]
struct DelegatedChildBootstrapView {
kind: &'static str,
mode: &'static str,
source_fingerprint: String,
task: String,
session_name_hint: String,
current_state: Vec<String>,
next_focus: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
remember: Vec<String>,
active_guardrails: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
key_files: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
close_out: Vec<String>,
shared_workspace: SharedWorkspaceView,
isolated_workspace: IsolatedWorkspaceView,
markdown: String,
}
#[derive(Serialize)]
struct SharedWorkspaceView {
posture: &'static str,
allowed_actions: Vec<&'static str>,
parent_only_commands: Vec<&'static str>,
}
#[derive(Serialize)]
struct IsolatedWorkspaceView {
status: &'static str,
posture: &'static str,
happy_path: Vec<String>,
}
struct MarkdownSections<'a> {
current_state: &'a [String],
next_focus: &'a [String],
remember: &'a [String],
active_guardrails: &'a [String],
key_files: &'a [String],
close_out: &'a [String],
shared_workspace: &'a SharedWorkspaceView,
isolated_workspace: &'a IsolatedWorkspaceView,
}
impl CommandReport for RuntimeStateChildBootstrapReport {
fn exit_code(&self) -> ExitCode {
ExitCode::SUCCESS
}
fn render_text(&self) {
print!("{}", self.child_bootstrap.markdown);
}
}
pub fn run(
repo_root: &Path,
explicit_profile: Option<&str>,
) -> Result<RuntimeStateChildBootstrapReport> {
let profile = profile::resolve(explicit_profile)?;
let layout = StateLayout::resolve(repo_root, profile.clone())?;
ensure_profile_exists(&layout, repo_root)?;
let locality_id = ensure_repo_linked(repo_root)?;
let runtime = runtime_state::load_runtime_state(repo_root, &layout, &locality_id)?;
let bundle = compiled_state::preview_bundle_for_target_with_cache(
&layout,
&runtime,
compiled_state::ProjectionTarget::Default,
)?
.value;
let tracked_session = session_state::load_for_layout(&layout)?;
let git = handoff::read_git_state(repo_root, BranchMode::AllowDetachedHead).ok();
let title = if runtime.state.handoff.title.trim().is_empty() {
format!("Next Session: {}", bundle.task)
} else {
runtime.state.handoff.title.trim().to_owned()
};
let current_state = build_current_state(&title, git.as_ref(), tracked_session.as_ref());
let next_focus = build_next_focus(&bundle);
let remember = bundle_section_items(&bundle, BundleSectionKind::Remember);
let key_files = bundle_section_items(&bundle, BundleSectionKind::Files);
let close_out = bundle_section_items(&bundle, BundleSectionKind::CloseOut);
let active_guardrails = build_guardrails(&bundle);
let session_name_hint = derive_session_name(&bundle.task, &next_focus);
let isolated_workspace = build_isolated_workspace_view(git.as_ref(), &session_name_hint);
let shared_workspace = SharedWorkspaceView {
posture: "read_only_investigation_only",
allowed_actions: vec![
"Consume this bootstrap bundle instead of rerunning `ccd start` in the shared workspace.",
"Inspect files and run non-mutating repo tools to gather findings or draft patches.",
"Return bounded results to the parent for integration and canonical CCD follow-through.",
],
parent_only_commands: PARENT_ONLY_COMMANDS.to_vec(),
};
let markdown = render_markdown(
&bundle.task,
&MarkdownSections {
current_state: ¤t_state,
next_focus: &next_focus,
remember: &remember,
active_guardrails: &active_guardrails,
key_files: &key_files,
close_out: &close_out,
shared_workspace: &shared_workspace,
isolated_workspace: &isolated_workspace,
},
);
Ok(RuntimeStateChildBootstrapReport {
command: "runtime-state-child-bootstrap",
ok: true,
path: repo_root.display().to_string(),
profile: profile.to_string(),
project_id: locality_id.clone(),
locality_id,
child_bootstrap: DelegatedChildBootstrapView {
kind: "delegated_child_bootstrap",
mode: "host_driven",
source_fingerprint: bundle.source_fingerprint,
task: bundle.task,
session_name_hint,
current_state,
next_focus,
remember,
active_guardrails,
key_files,
close_out,
shared_workspace,
isolated_workspace,
markdown,
},
})
}
fn build_current_state(
title: &str,
git: Option<&handoff::GitState>,
tracked_session: Option<&session_state::SessionStateFile>,
) -> Vec<String> {
let mut current_state = vec![format!("Title: {title}")];
let session_id = tracked_session.and_then(|state| state.session_id.as_deref());
let live_state = handoff::current_system_state_lines(git, session_id);
let mode_line = tracked_session
.filter(|state| state.mode != session_state::SessionMode::General)
.map(|session| format!("Session mode: `{}`", session.mode.as_str()));
if let Some(mode_line) = mode_line {
current_state.extend(live_state.into_iter().take(1));
current_state.push(mode_line);
} else {
current_state.extend(live_state.into_iter().take(2));
}
current_state
}
fn build_next_focus(bundle: &BundleProjectionView) -> Vec<String> {
let mut items = bundle_section_items(bundle, BundleSectionKind::Do);
items.extend(bundle_section_items(bundle, BundleSectionKind::Focus));
let mut items = dedupe_preserving_order(items);
if items.len() > 4 {
items.truncate(4);
}
items
}
fn build_guardrails(bundle: &BundleProjectionView) -> Vec<String> {
let mut items = bundle_section_items(bundle, BundleSectionKind::Rules);
items.extend([
"Parent remains the authoritative owner of workspace-local continuity, handoff, recovery, and session telemetry.".to_owned(),
"Shared-workspace children stay read-only; protected CCD writes remain parent-only in this workspace.".to_owned(),
"Any delegated child that needs repo writes must use an isolated workspace with its own CCD state.".to_owned(),
"Parent integrates child results and performs canonical `ccd radar-state` or `ccd handoff write` follow-through.".to_owned(),
]);
let mut items = dedupe_preserving_order(items);
if items.len() > 6 {
items.truncate(6);
}
items
}
fn build_isolated_workspace_view(
git: Option<&handoff::GitState>,
session_name_hint: &str,
) -> IsolatedWorkspaceView {
if git.is_some() {
return IsolatedWorkspaceView {
status: "available",
posture: "required_for_repo_writes",
happy_path: vec![
format!(
"Open a sibling workspace with `ccd session open --path . --profile <profile> --worktree ../{session_name_hint} --branch session/{session_name_hint} --from <trunk>` before delegated repo writes."
),
"Run `ccd start --activate` inside that isolated workspace only.".to_owned(),
"Let the parent integrate the child result and perform canonical close-out in the parent workspace.".to_owned(),
],
};
}
IsolatedWorkspaceView {
status: "unsupported",
posture: "shared_workspace_only",
happy_path: vec![
"This workspace is not Git-backed, so `ccd session open` is unavailable here.".to_owned(),
"Keep delegated children read-only in the shared workspace until a separate non-Git isolation path exists.".to_owned(),
"Parent remains responsible for canonical CCD follow-through in the current workspace.".to_owned(),
],
}
}
fn bundle_section_items(bundle: &BundleProjectionView, kind: BundleSectionKind) -> Vec<String> {
bundle
.sections
.iter()
.find(|section| section.kind == kind)
.map(|section| section.items.clone())
.unwrap_or_default()
}
fn dedupe_preserving_order(items: Vec<String>) -> Vec<String> {
let mut seen = HashSet::new();
let mut deduped = Vec::new();
for item in items {
let marker = normalize_space(&item).to_ascii_lowercase();
if marker.is_empty() || !seen.insert(marker) {
continue;
}
deduped.push(item);
}
deduped
}
fn normalize_space(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn derive_session_name(task: &str, next_focus: &[String]) -> String {
let source = if task.trim().is_empty() {
next_focus
.first()
.map(String::as_str)
.unwrap_or("session focus")
} else {
task
};
let mut tokens = tokenize(source);
tokens.retain(|token| !SESSION_NAME_STOPWORDS.contains(&token.as_str()));
let meaningful = tokens
.iter()
.filter(|token| !SESSION_NAME_GENERIC_TOKENS.contains(&token.as_str()))
.cloned()
.collect::<Vec<_>>();
let chosen = if meaningful.is_empty() {
tokens
} else {
meaningful
};
let name = chosen.into_iter().take(3).collect::<Vec<_>>().join("-");
if name.is_empty() {
"session-focus".to_owned()
} else {
name
}
}
fn tokenize(source: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current = String::new();
for ch in source.chars() {
if ch.is_ascii_alphanumeric() {
current.push(ch.to_ascii_lowercase());
} else if !current.is_empty() {
tokens.push(std::mem::take(&mut current));
}
}
if !current.is_empty() {
tokens.push(current);
}
tokens
}
fn render_markdown(task: &str, sections: &MarkdownSections<'_>) -> String {
let mut body = String::from("# CCD Delegated Child Bootstrap\n\n");
body.push_str(&format!("Task: {task}\n\n"));
push_markdown_section(&mut body, "Current state", sections.current_state);
push_markdown_section(&mut body, "Next focus", sections.next_focus);
push_markdown_section(&mut body, "Remember", sections.remember);
push_markdown_section(&mut body, "Active guardrails", sections.active_guardrails);
push_markdown_section(&mut body, "Files", sections.key_files);
push_markdown_section(&mut body, "Close-out", sections.close_out);
let mut shared_items = vec![format!("Posture: `{}`", sections.shared_workspace.posture)];
shared_items.extend(
sections
.shared_workspace
.allowed_actions
.iter()
.map(|item| (*item).to_owned()),
);
shared_items.push(format!(
"Parent-only commands: {}",
sections.shared_workspace.parent_only_commands.join(", ")
));
push_markdown_section(&mut body, "Shared workspace", &shared_items);
let mut isolated_items = vec![format!("Status: `{}`", sections.isolated_workspace.status)];
isolated_items.push(format!(
"Posture: `{}`",
sections.isolated_workspace.posture
));
isolated_items.extend(sections.isolated_workspace.happy_path.clone());
push_markdown_section(&mut body, "Isolated workspace", &isolated_items);
body
}
fn push_markdown_section(body: &mut String, title: &str, items: &[String]) {
if items.is_empty() {
return;
}
body.push_str(&format!("## {title}\n"));
for item in items {
body.push_str("- ");
body.push_str(item);
body.push('\n');
}
body.push('\n');
}
fn ensure_profile_exists(layout: &StateLayout, repo_root: &Path) -> Result<()> {
let profile_root = layout.profile_root();
if profile_root.is_dir() {
return Ok(());
}
bail!(
"profile `{}` does not exist at {}; bootstrap it with `ccd attach --path {}` before using `ccd runtime-state child-bootstrap`",
layout.profile(),
profile_root.display(),
repo_root.display()
)
}
fn ensure_repo_linked(repo_root: &Path) -> Result<String> {
let Some(marker) = repo_marker::load(repo_root)? else {
bail!(
"repo is not linked: {} is missing; run `ccd attach --path {}` or `ccd link --path {}` first",
repo_root.join(repo_marker::MARKER_FILE).display(),
repo_root.display(),
repo_root.display()
)
};
Ok(marker.locality_id)
}