use crate::entry_policy::normalize_entry_policy_mode_value;
use crate::request_routing::{default_routing_config, default_tool_routing_config};
use crate::{
create_timestamped_backup, read_optional_json_document, read_optional_toml_document,
resolve_legacy_shared_json_config_path, resolve_legacy_shared_toml_config_path,
resolve_previous_shared_config_path_for, resolve_shared_config_path,
timestamped_backup_path_for, write_toml_document,
};
use serde_json::{json, Value};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
pub(crate) const CURRENT_GENERATED_DEFAULTS_VERSION: u64 = 22;
const DEFAULT_ENTRY_POLICY_MODE: &str = "codex_cli_ccc_first";
const UNSUPPORTED_ENTRY_POLICY_FALLBACK_MODE: &str = "guided_explicit";
fn managed_agent_config(
name: &str,
summary: &str,
model: &str,
variant: &str,
fast_mode: bool,
role_metadata: Value,
) -> Value {
let mut config = json!({
"name": name,
"summary": summary,
"model": model,
"variant": variant,
"fast_mode": fast_mode,
"config_entries": []
});
merge_missing_config_fields(&mut config, &role_metadata);
config
}
fn editable_agent_config(name: &str, model: &str, variant: &str, fast_mode: bool) -> Value {
json!({
"name": name,
"model": model,
"variant": variant,
"fast_mode": fast_mode,
})
}
fn editable_ghost_agent_config() -> Value {
let mut config = editable_agent_config("ghost", "gpt-5.5", "medium", true);
if let Some(entries) = config.as_object_mut() {
entries.insert(
"sandbox_mode".to_string(),
Value::String("read-only".to_string()),
);
}
config
}
fn role_metadata_for_config_key(key: &str) -> Option<Value> {
let (display_name, callsign, workflows, lsp_capabilities) = match key {
"way" | "planner" => (
"Executor",
"Executor",
vec!["hyperplan"],
vec!["lsp_diagnostics", "lsp_definition"],
),
"explorer" => (
"Observer",
"Observer",
vec!["github-triage", "get-unpublished-changes"],
vec!["lsp_diagnostics", "lsp_references", "lsp_definition"],
),
"code specialist" => (
"Marauder",
"Marauder",
vec![
"remove-deadcode",
"ai-slop-remover",
"lsp-safe-refactor",
"rust-analyzer-lsp",
],
vec![
"lsp_diagnostics",
"lsp_references",
"lsp_definition",
"lsp_prepare_rename",
"lsp_rename",
"rust-analyzer-lsp",
],
),
"documenter" => (
"Adjutant",
"Adjutant",
vec!["release-note", "readme-maintenance", "changelog"],
vec!["lsp_diagnostics"],
),
"verifier" => (
"Arbiter",
"Arbiter",
vec!["review-work", "pre-publish-review"],
vec!["lsp_diagnostics", "lsp_references", "lsp_definition"],
),
"product-design" | "designer" | "product_design" => (
"Oracle",
"Oracle",
vec!["ui-ux-review", "accessibility-review", "visual-polish"],
vec!["lsp_diagnostics"],
),
"sentinel" => (
"Overseer",
"Overseer",
vec!["role-ownership", "lane-conflict", "fallback-classification"],
vec!["lsp_diagnostics"],
),
"companion_reader" => (
"Probe",
"Probe",
vec![
"github-triage",
"filesystem-evidence",
"get-unpublished-changes",
],
vec!["lsp_diagnostics", "lsp_references", "lsp_definition"],
),
"companion_operator" => (
"SCV",
"SCV",
vec!["git-master", "publish", "release-command-discipline"],
Vec::new(),
),
"ghost" => (
"Ghost",
"Ghost",
vec!["captain-drift-watchdog", "policy-drift-evidence"],
vec!["lsp_diagnostics"],
),
_ => return None,
};
Some(json!({
"display_name": display_name,
"callsign": callsign,
"theme": "starcraft_display_callsign",
"inspired_by": ["oh-my-openagent"],
"recommended_workflows": workflows,
"lsp_capabilities": lsp_capabilities,
}))
}
fn default_sentinel_agent_config() -> Value {
managed_agent_config(
"sentinel",
"Ownership and execution-path classification for bounded routing decisions.",
"gpt-5.4-mini",
"low",
true,
role_metadata_for_config_key("sentinel").expect("sentinel metadata"),
)
}
fn default_companion_agents_config() -> Value {
json!({
"companion_reader": managed_agent_config(
"companion_reader",
"Low-cost read-only tool work for filesystem, docs, web, git/gh inspection, and evidence gathering.",
"gpt-5.4-mini",
"low",
true,
role_metadata_for_config_key("companion_reader").expect("companion reader metadata"),
),
"companion_operator": managed_agent_config(
"companion_operator",
"Low-cost bounded operator work for git/gh mutation, release commands, and other narrow tool execution.",
"gpt-5.4-mini",
"low",
true,
role_metadata_for_config_key("companion_operator").expect("companion operator metadata"),
)
})
}
fn default_specialist_agents_config() -> Value {
json!({
"way": managed_agent_config(
"tactician",
"Way creation and bounded planning when the next move is still unclear.",
"gpt-5.5",
"high",
true,
role_metadata_for_config_key("way").expect("way metadata"),
),
"explorer": managed_agent_config(
"scout",
"Read-only repo investigation and evidence gathering.",
"gpt-5.4-mini",
"low",
true,
role_metadata_for_config_key("explorer").expect("explorer metadata"),
),
"code specialist": managed_agent_config(
"raider",
"Bounded code and config mutation for implementation and repair.",
"gpt-5.5",
"high",
true,
role_metadata_for_config_key("code specialist").expect("raider metadata"),
),
"documenter": managed_agent_config(
"scribe",
"Docs and operator-facing text updates.",
"gpt-5.4-mini",
"low",
true,
role_metadata_for_config_key("documenter").expect("documenter metadata"),
),
"verifier": managed_agent_config(
"arbiter",
"Review, regression detection, and acceptance judgment when needed.",
"gpt-5.5",
"high",
true,
role_metadata_for_config_key("verifier").expect("verifier metadata"),
),
"product-design": managed_agent_config(
"oracle",
"UI/UX, accessibility, layout, interaction quality, and visual-polish review.",
"gpt-5.5",
"high",
true,
role_metadata_for_config_key("product-design").expect("product-design metadata"),
),
"sentinel": default_sentinel_agent_config(),
"ghost": managed_agent_config(
"ghost",
"Read-only Captain behavior watchdog and drift monitoring over time.",
"gpt-5.5",
"medium",
true,
role_metadata_for_config_key("ghost").expect("ghost metadata"),
)
})
}
fn default_lsp_config() -> Value {
json!({
"enabled": true,
"runtime_execution": "bounded_readiness",
"capabilities": [
"lsp_diagnostics",
"lsp_references",
"lsp_definition",
"lsp_prepare_rename",
"lsp_rename",
"rust-analyzer-lsp"
],
"language_servers": {
"typescript_javascript": {
"command": "typescript-language-server",
"args": ["--stdio"],
"package_hint": "npm install -g typescript typescript-language-server",
"file_extensions": ["ts", "tsx", "js", "jsx", "mjs", "cjs"]
},
"rust": {
"command": "rust-analyzer",
"args": [],
"package_hint": "rustup component add rust-analyzer",
"file_extensions": ["rs"]
}
}
})
}
fn default_features_config() -> Value {
json!({
"graph_context": true,
"goals": true,
"prompt_refinement": true
})
}
fn default_goal_bridge_config() -> Value {
json!({
"enabled": true,
"mode": "captain_owned",
"brief_language": "en",
"brief_max_lines": 12,
"require_verifiable_stop": true,
"host_goal_state_is_truth": false,
"specialists": {
"allow_specialist_goal_context": true,
"allow_specialist_set_goal": false,
"allow_specialist_clear_goal": false,
"allow_specialist_override_goal": false,
"max_subgoal_lines": 8,
"require_captain_acceptance": true
}
})
}
fn default_graph_context_config() -> Value {
json!({
"enabled": true,
"opt_in": false,
"provider": "graphify",
"mode": "read_only",
"canonical_backend": "graphify",
"replace_legacy_ccc_graph_backend": true,
"allow_legacy_graph_backend_fallback": false,
"fallback_when_unavailable": "scout_source_evidence",
"report_path": "graphify-out/GRAPH_REPORT.md",
"graph_path": "graphify-out/graph.json",
"max_report_bytes": 20000,
"max_query_bytes": 8000,
"prefer_report_before_grep": true,
"allow_cli_query": true,
"allow_mcp_query": false,
"allow_rebuild": false,
"auto_install_external_dependency": false,
"source_of_truth": false,
"install": {
"managed_by_ccc_setup": true,
"check_install_reports_readiness": true,
"require_graphify_cli_for_queries": true,
"allow_missing_provider_fallback": true
},
"edges": {
"allow_extracted": true,
"allow_inferred": true,
"allow_ambiguous": false,
"require_source_check_for_mutation": true
}
})
}
fn default_generated_defaults_policy() -> Value {
json!({
"version": CURRENT_GENERATED_DEFAULTS_VERSION,
"policy": "ccc-managed-defaults",
})
}
fn default_runtime_config() -> Value {
json!({
"preferred_specialist_execution_mode": "codex_subagent",
"fallback_specialist_execution_mode": "codex_exec",
"worker_poll_interval_ms": 90000,
"worker_stuck_after_ms": 45000,
"worker_kill_grace_ms": 2000,
"worker_auto_reclaim_enabled": true,
"worker_max_retries_per_phase": 1,
"worker_retry_backoff_ms": 1000,
"worker_prompt_scope_max_chars": 320,
"worker_prompt_acceptance_max_chars": 220,
"worker_prompt_task_max_chars": 720,
"run_lock_stale_after_ms": 300000,
})
}
fn default_created_config() -> Value {
json!({
"version": env!("CARGO_PKG_VERSION"),
"generated_defaults": default_generated_defaults_policy(),
"entry_policy": {
"mode": DEFAULT_ENTRY_POLICY_MODE,
"auto_entry": {
"enabled": false
}
},
"output": {
"verbosity": "default"
},
"lsp": default_lsp_config(),
"graph_context": default_graph_context_config(),
"agents": {
"way": editable_agent_config("tactician", "gpt-5.5", "high", true),
"explorer": editable_agent_config("scout", "gpt-5.4-mini", "low", true),
"code specialist": editable_agent_config("raider", "gpt-5.5", "high", true),
"documenter": editable_agent_config("scribe", "gpt-5.4-mini", "low", true),
"product-design": editable_agent_config("oracle", "gpt-5.5", "high", true),
"verifier": editable_agent_config("arbiter", "gpt-5.5", "high", true),
"ghost": editable_ghost_agent_config()
}
})
}
#[derive(Clone, Copy, Debug)]
struct SetupMigrationDeltaFieldSpec {
path: &'static str,
label: &'static str,
expected: Option<&'static str>,
}
fn bool_expected(value: bool) -> &'static str {
if value {
"true"
} else {
"false"
}
}
fn setup_migration_delta_families() -> Vec<(
&'static str,
&'static str,
Vec<SetupMigrationDeltaFieldSpec>,
)> {
vec![
(
"graph",
"Graph context and Graphify fallback settings",
vec![
SetupMigrationDeltaFieldSpec {
path: "/features/graph_context",
label: "features.graph_context",
expected: Some(bool_expected(true)),
},
SetupMigrationDeltaFieldSpec {
path: "/graph_context/enabled",
label: "graph_context.enabled",
expected: Some(bool_expected(true)),
},
SetupMigrationDeltaFieldSpec {
path: "/graph_context/provider",
label: "graph_context.provider",
expected: Some("graphify"),
},
SetupMigrationDeltaFieldSpec {
path: "/graph_context/canonical_backend",
label: "graph_context.canonical_backend",
expected: Some("graphify"),
},
SetupMigrationDeltaFieldSpec {
path: "/graph_context/allow_legacy_graph_backend_fallback",
label: "graph_context.allow_legacy_graph_backend_fallback",
expected: Some(bool_expected(false)),
},
SetupMigrationDeltaFieldSpec {
path: "/graph_context/fallback_when_unavailable",
label: "graph_context.fallback_when_unavailable",
expected: Some("scout_source_evidence"),
},
],
),
(
"goal",
"Captain-owned goal bridge settings",
vec![
SetupMigrationDeltaFieldSpec {
path: "/features/goals",
label: "features.goals",
expected: Some(bool_expected(true)),
},
SetupMigrationDeltaFieldSpec {
path: "/goal_bridge/enabled",
label: "goal_bridge.enabled",
expected: Some(bool_expected(true)),
},
SetupMigrationDeltaFieldSpec {
path: "/goal_bridge/mode",
label: "goal_bridge.mode",
expected: Some("captain_owned"),
},
SetupMigrationDeltaFieldSpec {
path: "/goal_bridge/specialists/allow_specialist_set_goal",
label: "goal_bridge.specialists.allow_specialist_set_goal",
expected: Some(bool_expected(false)),
},
],
),
(
"prompt_refinement",
"Prompt-refinement feature flag",
vec![SetupMigrationDeltaFieldSpec {
path: "/features/prompt_refinement",
label: "features.prompt_refinement",
expected: Some(bool_expected(true)),
}],
),
(
"lsp",
"LSP bounded-readiness settings",
vec![
SetupMigrationDeltaFieldSpec {
path: "/lsp/enabled",
label: "lsp.enabled",
expected: Some(bool_expected(true)),
},
SetupMigrationDeltaFieldSpec {
path: "/lsp/runtime_execution",
label: "lsp.runtime_execution",
expected: Some("bounded_readiness"),
},
],
),
(
"fallback_execution",
"Specialist fallback execution settings",
vec![
SetupMigrationDeltaFieldSpec {
path: "/runtime/preferred_specialist_execution_mode",
label: "runtime.preferred_specialist_execution_mode",
expected: Some("codex_subagent"),
},
SetupMigrationDeltaFieldSpec {
path: "/runtime/fallback_specialist_execution_mode",
label: "runtime.fallback_specialist_execution_mode",
expected: Some("codex_exec"),
},
],
),
]
}
fn string_value_for_delta(value: &Value) -> Option<String> {
match value {
Value::String(value) => Some(value.clone()),
Value::Bool(value) => Some(value.to_string()),
Value::Number(value) => Some(value.to_string()),
Value::Null => None,
other => Some(other.to_string()),
}
}
fn pointer_string_for_delta(config: &Value, pointer: &str) -> Option<String> {
config.pointer(pointer).and_then(string_value_for_delta)
}
fn setup_migration_delta_field(
before: &Value,
after: &Value,
spec: SetupMigrationDeltaFieldSpec,
) -> Value {
let before_value = pointer_string_for_delta(before, spec.path);
let after_value = pointer_string_for_delta(after, spec.path);
let expected_value = spec.expected.map(str::to_string);
let changed = before_value != after_value;
let action = match (&before_value, &after_value, &expected_value) {
(None, Some(_), _) => "backfilled",
(Some(_), None, _) => "removed",
(Some(_), Some(_), _) if changed => "migrated",
(Some(value), Some(_), Some(expected)) if value != expected => "preserved_user_override",
(Some(_), Some(_), Some(_)) => "preserved_current",
(Some(_), Some(_), None) => "preserved",
(None, None, _) => "absent_using_runtime_default",
};
json!({
"field": spec.label,
"path": spec.path,
"action": action,
"changed": changed,
"before": before_value.map(Value::String).unwrap_or(Value::Null),
"after": after_value.map(Value::String).unwrap_or(Value::Null),
"expected": expected_value.map(Value::String).unwrap_or(Value::Null),
})
}
fn setup_migration_delta_family(
before: &Value,
after: &Value,
family: &'static str,
description: &'static str,
specs: Vec<SetupMigrationDeltaFieldSpec>,
) -> Value {
let fields = specs
.into_iter()
.map(|spec| setup_migration_delta_field(before, after, spec))
.collect::<Vec<_>>();
let count_action = |action: &str| {
fields
.iter()
.filter(|field| field.get("action").and_then(Value::as_str) == Some(action))
.count()
};
let changed_count = fields
.iter()
.filter(|field| {
field
.get("changed")
.and_then(Value::as_bool)
.unwrap_or(false)
})
.count();
let backfilled_count = count_action("backfilled");
let migrated_count = count_action("migrated");
let preserved_override_count = count_action("preserved_user_override");
let absent_runtime_default_count = count_action("absent_using_runtime_default");
let status = if migrated_count > 0 {
if preserved_override_count > 0 {
"migrated_with_preserved_overrides"
} else {
"migrated"
}
} else if backfilled_count > 0 {
if preserved_override_count > 0 {
"backfilled_with_preserved_overrides"
} else {
"backfilled"
}
} else if preserved_override_count > 0 {
"preserved_user_override"
} else if absent_runtime_default_count == fields.len() {
"absent_using_runtime_defaults"
} else {
"preserved_current"
};
json!({
"family": family,
"status": status,
"description": description,
"changed_count": changed_count,
"backfilled_count": backfilled_count,
"migrated_count": migrated_count,
"preserved_override_count": preserved_override_count,
"absent_runtime_default_count": absent_runtime_default_count,
"fields": fields,
})
}
fn setup_migration_delta_payload(before: &Value, after: &Value, action_status: &str) -> Value {
let families = setup_migration_delta_families()
.into_iter()
.map(|(family, description, specs)| {
setup_migration_delta_family(before, after, family, description, specs)
})
.collect::<Vec<_>>();
let sum_family_count = |key: &str| {
families
.iter()
.map(|family| family.get(key).and_then(Value::as_u64).unwrap_or(0))
.sum::<u64>()
};
let backfilled_count = sum_family_count("backfilled_count");
let migrated_count = sum_family_count("migrated_count");
let preserved_override_count = sum_family_count("preserved_override_count");
let status = if migrated_count > 0 {
"migrated"
} else if backfilled_count > 0 {
"backfilled"
} else if preserved_override_count > 0 {
"preserved_user_override"
} else {
"preserved_current"
};
json!({
"schema": "ccc.setup_migration_deltas.v1",
"status": status,
"action_status": action_status,
"family_count": families.len(),
"changed_count": sum_family_count("changed_count"),
"backfilled_count": backfilled_count,
"migrated_count": migrated_count,
"preserved_override_count": preserved_override_count,
"families": families,
"summary": "Setup migration deltas explicitly report graph, goal, prompt-refinement, LSP, and fallback execution settings instead of silently normalizing them.",
})
}
fn merge_missing_config_fields(target: &mut Value, defaults: &Value) -> bool {
let (Value::Object(target_entries), Value::Object(default_entries)) = (target, defaults) else {
return false;
};
let mut changed = false;
for (key, default_value) in default_entries {
match target_entries.get_mut(key) {
Some(existing_value) if existing_value.is_null() => {
*existing_value = default_value.clone();
changed = true;
}
Some(existing_value) => {
if merge_missing_config_fields(existing_value, default_value) {
changed = true;
}
}
None => {
target_entries.insert(key.clone(), default_value.clone());
changed = true;
}
}
}
changed
}
fn generated_defaults_version(config: &Value) -> u64 {
config
.get("generated_defaults")
.and_then(|value| value.get("version"))
.and_then(Value::as_u64)
.unwrap_or(0)
}
fn entry_policy_mode_backfill_reason(config: &Value) -> Option<String> {
let entry_policy = config.get("entry_policy")?;
let Some(entry_policy_object) = entry_policy.as_object() else {
return Some(
"Entry policy config must be an object with a supported mode; run setup to backfill `codex_cli_ccc_first`."
.to_string(),
);
};
let Some(mode_value) = entry_policy_object.get("mode") else {
return Some(
"Entry policy config is missing `mode`; run setup to backfill `codex_cli_ccc_first`."
.to_string(),
);
};
let Some(raw_mode) = mode_value.as_str() else {
return Some(
"Entry policy mode must be a string; run setup to backfill `codex_cli_ccc_first`."
.to_string(),
);
};
match normalize_entry_policy_mode_value(raw_mode) {
Some(canonical_mode) if canonical_mode == raw_mode => None,
Some(canonical_mode) => Some(format!(
"Entry policy mode `{raw_mode}` is a legacy alias for `{canonical_mode}`; run setup to backfill the canonical value."
)),
None => Some(format!(
"Entry policy mode `{raw_mode}` is not supported; run setup to backfill `guided_explicit`."
)),
}
}
fn entry_policy_mode_visibility(
config: &Value,
) -> (&'static str, Option<String>, Option<String>, String) {
let Some(entry_policy) = config.get("entry_policy") else {
return (
"missing/backfill-needed",
None,
Some(DEFAULT_ENTRY_POLICY_MODE.to_string()),
"Entry policy config is missing; run setup to backfill `codex_cli_ccc_first`."
.to_string(),
);
};
let Some(entry_policy_object) = entry_policy.as_object() else {
return (
"invalid/backfill-needed",
None,
Some(DEFAULT_ENTRY_POLICY_MODE.to_string()),
"Entry policy config must be an object with a supported mode; run setup to backfill `codex_cli_ccc_first`."
.to_string(),
);
};
let Some(mode_value) = entry_policy_object.get("mode") else {
return (
"missing/backfill-needed",
None,
Some(DEFAULT_ENTRY_POLICY_MODE.to_string()),
"Entry policy mode is missing; run setup to backfill `codex_cli_ccc_first`."
.to_string(),
);
};
let Some(raw_mode) = mode_value.as_str() else {
return (
"invalid/backfill-needed",
None,
Some(DEFAULT_ENTRY_POLICY_MODE.to_string()),
"Entry policy mode must be a string; run setup to backfill `codex_cli_ccc_first`."
.to_string(),
);
};
match normalize_entry_policy_mode_value(raw_mode) {
Some(canonical_mode) if canonical_mode == raw_mode => (
"canonical",
Some(raw_mode.to_string()),
Some(canonical_mode.to_string()),
format!("Entry policy mode `{raw_mode}` is canonical and supported."),
),
Some(canonical_mode) => (
"legacy/backfill-needed",
Some(raw_mode.to_string()),
Some(canonical_mode.to_string()),
format!(
"Entry policy mode `{raw_mode}` is runtime-compatible as `{canonical_mode}`, but setup should backfill the canonical value."
),
),
None => (
"invalid/unsupported",
Some(raw_mode.to_string()),
Some("guided_explicit".to_string()),
format!(
"Entry policy mode `{raw_mode}` is unsupported; runtime falls back to `guided_explicit`, and setup should backfill it."
),
),
}
}
fn unavailable_entry_policy_mode_visibility(
) -> (&'static str, Option<String>, Option<String>, String) {
(
"unavailable",
None,
None,
"Entry policy mode health is unavailable because no readable canonical CCC config was loaded."
.to_string(),
)
}
fn config_install_state(
status: &'static str,
action_status: &'static str,
backup_status: &'static str,
summary: String,
source_path: Option<PathBuf>,
backup_source_path: Option<PathBuf>,
backup_path: Option<PathBuf>,
value: Value,
canonical_ready: bool,
config_exists: bool,
restart_status: &'static str,
) -> CccConfigInstallState {
let (
entry_policy_mode_status,
entry_policy_mode_raw,
entry_policy_mode_canonical,
entry_policy_mode_summary,
) = if canonical_ready && config_exists {
entry_policy_mode_visibility(&value)
} else {
unavailable_entry_policy_mode_visibility()
};
let setup_migration_deltas = setup_migration_delta_payload(&value, &value, action_status);
CccConfigInstallState {
status,
action_status,
backup_status,
summary,
source_path,
backup_source_path,
backup_path,
value,
canonical_ready,
config_exists,
restart_status,
entry_policy_mode_status,
entry_policy_mode_raw,
entry_policy_mode_canonical,
entry_policy_mode_summary,
setup_migration_deltas,
}
}
fn backfill_entry_policy_mode(config_entries: &mut serde_json::Map<String, Value>) -> bool {
let Some(entry_policy) = config_entries.get_mut("entry_policy") else {
return false;
};
let Some(entry_policy_object) = entry_policy.as_object_mut() else {
*entry_policy = json!({
"mode": DEFAULT_ENTRY_POLICY_MODE,
"auto_entry": {
"enabled": false
}
});
return true;
};
let mut changed = false;
let current_mode = entry_policy_object.get("mode").and_then(Value::as_str);
let next_mode = current_mode
.map(|mode| {
normalize_entry_policy_mode_value(mode)
.unwrap_or(UNSUPPORTED_ENTRY_POLICY_FALLBACK_MODE)
})
.unwrap_or(DEFAULT_ENTRY_POLICY_MODE);
if current_mode != Some(next_mode) {
entry_policy_object.insert("mode".to_string(), Value::String(next_mode.to_string()));
changed = true;
}
if !entry_policy_object
.get("auto_entry")
.and_then(Value::as_object)
.and_then(|auto_entry| auto_entry.get("enabled"))
.is_some_and(Value::is_boolean)
{
entry_policy_object.insert(
"auto_entry".to_string(),
json!({
"enabled": false
}),
);
changed = true;
}
changed
}
fn backfill_shared_config_version(config_entries: &mut serde_json::Map<String, Value>) -> bool {
let current_version = Value::String(env!("CARGO_PKG_VERSION").to_string());
match config_entries.get_mut("version") {
Some(existing) if existing == ¤t_version => false,
Some(existing) if existing.is_object() || existing.is_array() => false,
Some(existing) => {
*existing = current_version;
true
}
None => {
config_entries.insert("version".to_string(), current_version);
true
}
}
}
fn old_generated_mini_role_default_spec(
path: &[&str],
) -> Option<(
&'static str,
&'static str,
&'static str,
&'static [&'static str],
)> {
match path {
["agents", "explorer"] => Some((
"scout",
"Read-only repo investigation and evidence gathering.",
"gpt-5.4-mini",
&["high"],
)),
["agents", "documenter"] => Some((
"scribe",
"Docs and operator-facing text updates.",
"gpt-5.4-mini",
&["medium"],
)),
["agents", "sentinel"] => Some((
"sentinel",
"Ownership and execution-path classification for bounded routing decisions.",
"gpt-5.4-mini",
&["high", "medium"],
)),
["companion_agents", "companion_reader"] => Some((
"companion_reader",
"Low-cost read-only tool work for filesystem, docs, web, git/gh inspection, and evidence gathering.",
"gpt-5.4-mini",
&["medium"],
)),
["companion_agents", "companion_operator"] => Some((
"companion_operator",
"Low-cost bounded operator work for git/gh mutation, release commands, and other narrow tool execution.",
"gpt-5.4-mini",
&["medium"],
)),
_ => None,
}
}
fn is_old_generated_mini_role_default(
path: &[&str],
object: &serde_json::Map<String, Value>,
) -> bool {
let Some((expected_name, expected_summary, expected_model, expected_variants)) =
old_generated_mini_role_default_spec(path)
else {
return false;
};
let expected_variant = object
.get("variant")
.and_then(Value::as_str)
.filter(|variant| expected_variants.iter().any(|expected| expected == variant));
expected_variant.is_some()
&& object.get("name").and_then(Value::as_str) == Some(expected_name)
&& object.get("summary").and_then(Value::as_str) == Some(expected_summary)
&& object.get("model").and_then(Value::as_str) == Some(expected_model)
&& object.get("fast_mode").and_then(Value::as_bool) == Some(true)
&& object
.get("config_entries")
.and_then(Value::as_array)
.is_some_and(|entries| entries.is_empty())
}
fn is_old_generated_ghost_default(object: &serde_json::Map<String, Value>) -> bool {
let summary = object.get("summary").and_then(Value::as_str);
object.get("name").and_then(Value::as_str) == Some("ghost")
&& matches!(
summary,
Some("Read-only Captain behavior watchdog.")
| Some("Read-only Captain behavior watchdog and drift monitoring over time.")
)
&& object.get("model").and_then(Value::as_str) == Some("gpt-5.4-mini")
&& object.get("variant").and_then(Value::as_str) == Some("medium")
&& object.get("fast_mode").and_then(Value::as_bool) == Some(false)
&& object
.get("config_entries")
.and_then(Value::as_array)
.is_some_and(|entries| entries.is_empty())
&& object.get("sandbox_mode").is_none()
}
fn upgrade_role_generated_defaults(config: &mut Value, path: &[&str]) -> bool {
let mut current = config;
for key in path {
let Some(next) = current.get_mut(*key) else {
return false;
};
current = next;
}
let Some(object) = current.as_object_mut() else {
return false;
};
let mut changed = false;
if path == ["agents", "way"]
|| path == ["agents", "planner"]
|| path == ["agents", "verifier"]
|| path == ["agents", "product-design"]
|| path == ["agents", "designer"]
|| path == ["agents", "code specialist"]
{
if object.get("model").and_then(Value::as_str) == Some("gpt-5.5")
&& object.get("variant").and_then(Value::as_str) == Some("medium")
{
object.insert("variant".to_string(), Value::String("high".to_string()));
changed = true;
}
} else if path == ["agents", "ghost"] && is_old_generated_ghost_default(object) {
object.insert("model".to_string(), Value::String("gpt-5.5".to_string()));
object.insert("variant".to_string(), Value::String("medium".to_string()));
object.insert("fast_mode".to_string(), Value::Bool(true));
object.insert(
"sandbox_mode".to_string(),
Value::String("read-only".to_string()),
);
changed = true;
} else if is_old_generated_mini_role_default(path, object) {
if object.get("variant").and_then(Value::as_str) != Some("low") {
object.insert("variant".to_string(), Value::String("low".to_string()));
changed = true;
}
if object.get("fast_mode").and_then(Value::as_bool) != Some(true) {
object.insert("fast_mode".to_string(), Value::Bool(true));
changed = true;
}
}
changed
}
fn apply_generated_default_drift_upgrades(config: &mut Value) -> bool {
let existing_generated_defaults_version = generated_defaults_version(config);
let mut changed = false;
if existing_generated_defaults_version < CURRENT_GENERATED_DEFAULTS_VERSION {
for path in [
&["agents", "explorer"][..],
&["agents", "sentinel"][..],
&["agents", "documenter"][..],
&["agents", "code specialist"][..],
&["agents", "way"][..],
&["agents", "planner"][..],
&["agents", "verifier"][..],
&["agents", "product-design"][..],
&["agents", "designer"][..],
&["agents", "ghost"][..],
&["companion_agents", "companion_reader"][..],
&["companion_agents", "companion_operator"][..],
] {
if upgrade_role_generated_defaults(config, path) {
changed = true;
}
}
if let Some(runtime) = config.get_mut("runtime").and_then(Value::as_object_mut) {
if runtime
.get("fallback_specialist_execution_mode")
.and_then(Value::as_str)
== Some("visible_degraded_host_fallback")
{
runtime.insert(
"fallback_specialist_execution_mode".to_string(),
Value::String("codex_exec".to_string()),
);
changed = true;
}
}
if let Some(features) = config.get_mut("features").and_then(Value::as_object_mut) {
for key in ["graph_context", "goals", "prompt_refinement"] {
if features.get(key).and_then(Value::as_bool) == Some(false) {
features.insert(key.to_string(), Value::Bool(true));
changed = true;
}
}
}
if let Some(goal_bridge) = config.get_mut("goal_bridge").and_then(Value::as_object_mut) {
if goal_bridge.get("enabled").and_then(Value::as_bool) == Some(false) {
goal_bridge.insert("enabled".to_string(), Value::Bool(true));
changed = true;
}
}
if let Some(graph_context) = config
.get_mut("graph_context")
.and_then(Value::as_object_mut)
{
if graph_context.get("enabled").and_then(Value::as_bool) == Some(false) {
graph_context.insert("enabled".to_string(), Value::Bool(true));
changed = true;
}
}
if let Some(lsp) = config.get_mut("lsp").and_then(Value::as_object_mut) {
if lsp.get("enabled").and_then(Value::as_bool) == Some(false) {
lsp.insert("enabled".to_string(), Value::Bool(true));
changed = true;
}
if lsp.get("runtime_execution").and_then(Value::as_str) == Some("deferred") {
lsp.insert(
"runtime_execution".to_string(),
Value::String("bounded_readiness".to_string()),
);
lsp.remove("deferred_reason");
changed = true;
}
}
for (key, default_value) in [
("features", default_features_config()),
("goal_bridge", default_goal_bridge_config()),
("graph_context", default_graph_context_config()),
] {
match config.get_mut(key) {
Some(existing_value) if existing_value.is_null() => {
*existing_value = default_value;
changed = true;
}
Some(existing_value) => {
if merge_missing_config_fields(existing_value, &default_value) {
changed = true;
}
}
None => {
if let Some(config_entries) = config.as_object_mut() {
config_entries.insert(key.to_string(), default_value);
changed = true;
}
}
}
}
}
let Some(config_entries) = config.as_object_mut() else {
return changed;
};
let default_policy = default_generated_defaults_policy();
match config_entries.get_mut("generated_defaults") {
Some(existing) if existing.is_null() => {
*existing = default_policy;
changed = true;
}
Some(existing) => {
if merge_missing_config_fields(existing, &default_policy) {
changed = true;
}
if existing
.get("version")
.and_then(Value::as_u64)
.map(|version| version < CURRENT_GENERATED_DEFAULTS_VERSION)
.unwrap_or(true)
{
if let Some(object) = existing.as_object_mut() {
object.insert(
"version".to_string(),
Value::from(CURRENT_GENERATED_DEFAULTS_VERSION),
);
changed = true;
}
}
}
None => {
config_entries.insert("generated_defaults".to_string(), default_policy);
changed = true;
}
}
changed
}
fn backfill_generated_defaults(config: &mut Value) -> bool {
let Some(config_entries) = config.as_object_mut() else {
return false;
};
let mut changed = false;
if backfill_shared_config_version(config_entries) {
changed = true;
}
if backfill_entry_policy_mode(config_entries) {
changed = true;
}
let companion_defaults = default_companion_agents_config();
match config_entries.get_mut("companion_agents") {
Some(existing_value) if existing_value.is_null() => {
*existing_value = companion_defaults;
changed = true;
}
Some(existing_value) => {
if merge_missing_config_fields(existing_value, &companion_defaults) {
changed = true;
}
}
None => {
config_entries.insert("companion_agents".to_string(), companion_defaults);
changed = true;
}
}
let specialist_defaults = default_specialist_agents_config();
match config_entries.get_mut("agents") {
Some(existing_value) if existing_value.is_null() => {
*existing_value = specialist_defaults;
changed = true;
}
Some(existing_value) => {
if merge_missing_config_fields(existing_value, &specialist_defaults) {
changed = true;
}
}
None => {
config_entries.insert("agents".to_string(), specialist_defaults);
changed = true;
}
}
for (key, default_value) in [
("features", default_features_config()),
("goal_bridge", default_goal_bridge_config()),
("graph_context", default_graph_context_config()),
("routing", default_routing_config()),
("tool_routing", default_tool_routing_config()),
("runtime", default_runtime_config()),
("lsp", default_lsp_config()),
] {
match config_entries.get_mut(key) {
Some(existing_value) if existing_value.is_null() => {
*existing_value = default_value;
changed = true;
}
Some(existing_value) => {
if merge_missing_config_fields(existing_value, &default_value) {
changed = true;
}
}
None => {}
}
}
if apply_generated_default_drift_upgrades(config) {
changed = true;
}
changed
}
#[derive(Clone, Debug)]
pub(crate) struct CccConfigInstallState {
pub(crate) status: &'static str,
pub(crate) action_status: &'static str,
pub(crate) backup_status: &'static str,
pub(crate) summary: String,
pub(crate) source_path: Option<PathBuf>,
pub(crate) backup_source_path: Option<PathBuf>,
pub(crate) backup_path: Option<PathBuf>,
pub(crate) value: Value,
pub(crate) canonical_ready: bool,
pub(crate) config_exists: bool,
pub(crate) restart_status: &'static str,
pub(crate) entry_policy_mode_status: &'static str,
pub(crate) entry_policy_mode_raw: Option<String>,
pub(crate) entry_policy_mode_canonical: Option<String>,
pub(crate) entry_policy_mode_summary: String,
pub(crate) setup_migration_deltas: Value,
}
impl CccConfigInstallState {
pub(crate) fn source_path_value(&self) -> Value {
self.source_path
.as_ref()
.map(|path| Value::String(path.to_string_lossy().into_owned()))
.unwrap_or(Value::Null)
}
pub(crate) fn backup_source_path_value(&self) -> Value {
self.backup_source_path
.as_ref()
.map(|path| Value::String(path.to_string_lossy().into_owned()))
.unwrap_or(Value::Null)
}
pub(crate) fn backup_path_value(&self) -> Value {
self.backup_path
.as_ref()
.map(|path| Value::String(path.to_string_lossy().into_owned()))
.unwrap_or(Value::Null)
}
pub(crate) fn entry_policy_mode_raw_value(&self) -> Value {
self.entry_policy_mode_raw
.as_ref()
.map(|value| Value::String(value.clone()))
.unwrap_or(Value::Null)
}
pub(crate) fn entry_policy_mode_canonical_value(&self) -> Value {
self.entry_policy_mode_canonical
.as_ref()
.map(|value| Value::String(value.clone()))
.unwrap_or(Value::Null)
}
pub(crate) fn setup_migration_deltas_value(&self) -> Value {
self.setup_migration_deltas.clone()
}
}
fn read_optional_config_source(path: &Path, format: &str) -> io::Result<Option<(PathBuf, Value)>> {
let value = match format {
"json" => read_optional_json_document(path)?,
_ => read_optional_toml_document(path)?,
};
Ok(value.map(|value| (path.to_path_buf(), value)))
}
pub(crate) fn collect_ccc_config_install_state_at(
config_path: &Path,
legacy_toml_path: &Path,
legacy_json_path: &Path,
) -> io::Result<CccConfigInstallState> {
let canonical = read_optional_config_source(config_path, "toml")?;
let previous = resolve_previous_shared_config_path_for(config_path)
.filter(|previous_path| previous_path != config_path)
.map(|previous_path| read_optional_config_source(&previous_path, "toml"))
.transpose()?
.flatten();
let legacy_toml = read_optional_config_source(legacy_toml_path, "toml")?;
let legacy_json = read_optional_config_source(legacy_json_path, "json")?;
if let Some((canonical_path, canonical_value)) = canonical {
let conflicting_source = previous
.as_ref()
.filter(|(_, value)| value != &canonical_value)
.map(|(path, _)| path.clone());
if let Some(conflicting_source) = conflicting_source {
return Ok(config_install_state(
"conflict",
"preserved",
"not-required",
format!(
"Canonical CCC config is present at {}, but a legacy migration source at {} differs; setup preserves the canonical file.",
canonical_path.display(),
conflicting_source.display()
),
Some(canonical_path),
None,
None,
canonical_value.clone(),
true,
true,
"not-required",
));
}
let mut planned_value = canonical_value.clone();
let entry_policy_backfill_reason = entry_policy_mode_backfill_reason(&canonical_value);
if backfill_generated_defaults(&mut planned_value) {
let backfill_detail = entry_policy_backfill_reason
.map(|reason| format!(" {reason}"))
.unwrap_or_default();
let mut state = config_install_state(
"canonical-needs-backfill",
"setup-backfill-available",
"setup-backup-available",
format!(
"Canonical CCC config at {} is missing or has stale generated defaults; run setup to create a timestamped backup, backfill or upgrade generated defaults while preserving customized values, then restart Codex CLI.{}",
canonical_path.display(),
backfill_detail
),
Some(canonical_path.clone()),
Some(canonical_path.clone()),
Some(timestamped_backup_path_for(&canonical_path)),
canonical_value.clone(),
true,
true,
"restart-required-after-setup",
);
state.setup_migration_deltas = setup_migration_delta_payload(
&canonical_value,
&planned_value,
state.action_status,
);
return Ok(state);
}
return Ok(config_install_state(
"canonical-current",
"preserved",
"not-required",
format!(
"Canonical CCC config is current at {}.",
canonical_path.display()
),
Some(canonical_path),
None,
None,
canonical_value,
true,
true,
"not-required",
));
}
if let Some((source_path, value)) = previous.or(legacy_toml).or(legacy_json) {
let mut planned_value = value.clone();
backfill_generated_defaults(&mut planned_value);
let mut state = config_install_state(
"legacy-only",
"skipped",
"setup-backup-available",
format!(
"CCC config is only available from legacy source {}; run setup to create a timestamped backup, migrate it to the canonical path, then restart Codex CLI.",
source_path.display()
),
Some(source_path.clone()),
Some(source_path.clone()),
Some(timestamped_backup_path_for(&source_path)),
value.clone(),
false,
true,
"restart-required-after-setup",
);
state.setup_migration_deltas =
setup_migration_delta_payload(&value, &planned_value, "setup-migrate-available");
return Ok(state);
}
let mut state = config_install_state(
"missing",
"skipped",
"not-required",
format!(
"No CCC config was found at the canonical path {} or known legacy sources; run setup to create a generated default config, then restart Codex CLI.",
config_path.display()
),
None,
None,
None,
Value::Null,
false,
false,
"restart-required-after-setup",
);
state.setup_migration_deltas = setup_migration_delta_payload(
&Value::Null,
&default_created_config(),
"setup-create-available",
);
Ok(state)
}
pub(crate) fn plan_ccc_config_setup_at(
config_path: &Path,
legacy_toml_path: &Path,
legacy_json_path: &Path,
) -> io::Result<CccConfigInstallState> {
let mut plan =
collect_ccc_config_install_state_at(config_path, legacy_toml_path, legacy_json_path)?;
match plan.status {
"legacy-only" => {
plan.action_status = "would-migrate";
if let Some(delta) = plan.setup_migration_deltas.as_object_mut() {
delta.insert(
"action_status".to_string(),
Value::String(plan.action_status.to_string()),
);
}
plan.summary = format!(
"Setup would create a timestamped backup of {}, migrate it to {}, preserve customized values, and require a Codex CLI restart.",
plan.source_path
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "the legacy CCC config".to_string()),
config_path.display()
);
}
"canonical-needs-backfill" => {
plan.action_status = "would-backfill";
if let Some(delta) = plan.setup_migration_deltas.as_object_mut() {
delta.insert(
"action_status".to_string(),
Value::String(plan.action_status.to_string()),
);
}
plan.summary = format!(
"Setup would create a timestamped backup of {}, backfill or upgrade generated defaults while preserving customized values, and require a Codex CLI restart.",
config_path.display()
);
}
"missing" => {
plan.action_status = "would-create";
if let Some(delta) = plan.setup_migration_deltas.as_object_mut() {
delta.insert(
"action_status".to_string(),
Value::String(plan.action_status.to_string()),
);
}
plan.summary = format!(
"Setup would create generated defaults at {} and require a Codex CLI restart.",
config_path.display()
);
}
"conflict" => {
plan.summary.push_str(
" Dry-run will not resolve this conflict; setup preserves the canonical file.",
);
}
_ => {
plan.summary
.push_str(" Setup would not change the CCC config.");
}
}
Ok(plan)
}
#[derive(Clone, Debug)]
struct CccConfigApplyReport {
backup_status: &'static str,
backup_source_path: Option<PathBuf>,
backup_path: Option<PathBuf>,
}
impl CccConfigApplyReport {
fn not_required() -> Self {
Self {
backup_status: "not-required",
backup_source_path: None,
backup_path: None,
}
}
}
fn backup_config_apply_source(
source_path: &Path,
report: &mut CccConfigApplyReport,
) -> io::Result<()> {
let backup_path = create_timestamped_backup(source_path)?;
report.backup_status = "created";
report.backup_source_path = Some(source_path.to_path_buf());
report.backup_path = Some(backup_path);
Ok(())
}
pub(crate) fn rollback_ccc_config_from_backup_at(
config_path: &Path,
backup_path: &Path,
) -> io::Result<CccConfigInstallState> {
let metadata = fs::metadata(backup_path).map_err(|error| {
io::Error::new(
error.kind(),
format!(
"CCC config rollback backup {} is not readable: {error}",
backup_path.display()
),
)
})?;
if !metadata.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"CCC config rollback backup {} is not a file.",
backup_path.display()
),
));
}
let value = read_optional_toml_document(backup_path)?.unwrap_or(Value::Null);
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(backup_path, config_path)?;
Ok(config_install_state(
"rollback-restored",
"rolled-back",
"restored",
format!(
"CCC config rollback restored {} to the canonical path {}; restart Codex CLI to use the restored config.",
backup_path.display(),
config_path.display()
),
Some(config_path.to_path_buf()),
Some(backup_path.to_path_buf()),
Some(backup_path.to_path_buf()),
value,
true,
true,
"restart-required",
))
}
pub(crate) fn ensure_ccc_config_file_at(
config_path: &Path,
legacy_toml_path: &Path,
legacy_json_path: &Path,
) -> io::Result<(PathBuf, bool)> {
let (config_path, created, _) =
ensure_ccc_config_file_at_with_report(config_path, legacy_toml_path, legacy_json_path)?;
Ok((config_path, created))
}
fn ensure_ccc_config_file_at_with_report(
config_path: &Path,
legacy_toml_path: &Path,
legacy_json_path: &Path,
) -> io::Result<(PathBuf, bool, CccConfigApplyReport)> {
let mut report = CccConfigApplyReport::not_required();
if config_path.exists() {
if let Ok(Some(mut existing_config)) = read_optional_toml_document(config_path) {
if backfill_generated_defaults(&mut existing_config) {
backup_config_apply_source(config_path, &mut report)?;
write_toml_document(config_path, &existing_config)?;
}
}
return Ok((config_path.to_path_buf(), false, report));
}
if let Some(previous_path) = resolve_previous_shared_config_path_for(config_path) {
if previous_path != config_path {
if let Some(mut previous_config) = read_optional_toml_document(&previous_path)? {
backfill_generated_defaults(&mut previous_config);
backup_config_apply_source(&previous_path, &mut report)?;
write_toml_document(config_path, &previous_config)?;
return Ok((config_path.to_path_buf(), true, report));
}
}
}
if let Some(mut legacy_config) = read_optional_toml_document(legacy_toml_path)? {
backfill_generated_defaults(&mut legacy_config);
backup_config_apply_source(legacy_toml_path, &mut report)?;
write_toml_document(config_path, &legacy_config)?;
return Ok((config_path.to_path_buf(), true, report));
}
if let Some(mut legacy_config) = read_optional_json_document(legacy_json_path)? {
backfill_generated_defaults(&mut legacy_config);
backup_config_apply_source(legacy_json_path, &mut report)?;
write_toml_document(config_path, &legacy_config)?;
return Ok((config_path.to_path_buf(), true, report));
}
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)?;
}
let default_config = default_created_config();
write_toml_document(config_path, &default_config)?;
Ok((config_path.to_path_buf(), true, report))
}
pub(crate) fn ensure_ccc_config_file_at_with_state(
config_path: &Path,
legacy_toml_path: &Path,
legacy_json_path: &Path,
) -> io::Result<(PathBuf, bool, CccConfigInstallState)> {
let before =
collect_ccc_config_install_state_at(config_path, legacy_toml_path, legacy_json_path).ok();
let (created_path, created, apply_report) =
ensure_ccc_config_file_at_with_report(config_path, legacy_toml_path, legacy_json_path)?;
let mut state =
collect_ccc_config_install_state_at(config_path, legacy_toml_path, legacy_json_path)?;
state.restart_status = "restart-required";
state.backup_status = apply_report.backup_status;
state.backup_source_path = apply_report.backup_source_path;
state.backup_path = apply_report.backup_path;
if let Some(before) = before {
state.setup_migration_deltas =
setup_migration_delta_payload(&before.value, &state.value, state.action_status);
if created && before.status == "legacy-only" {
state.status = "migrated-from-previous";
state.action_status = "migrated-from-previous";
state.setup_migration_deltas =
setup_migration_delta_payload(&before.value, &state.value, state.action_status);
state.summary = format!(
"CCC config was migrated from {} to the canonical path {}; backup_status={}; restart Codex CLI to use the refreshed install surface.",
before
.source_path
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "a legacy source".to_string()),
config_path.display(),
state.backup_status
);
} else if created && before.status == "missing" {
state.status = "created";
state.action_status = "created";
state.backup_status = "not-required";
state.setup_migration_deltas =
setup_migration_delta_payload(&before.value, &state.value, state.action_status);
state.summary = format!(
"CCC config was created at {}; restart Codex CLI to use the refreshed install surface.",
config_path.display()
);
} else if !created && before.value != state.value {
state.status = "canonical-current";
state.action_status = "backfilled";
state.setup_migration_deltas =
setup_migration_delta_payload(&before.value, &state.value, state.action_status);
state.summary = format!(
"Canonical CCC config at {} was backfilled with missing defaults after creating a timestamped backup; restart Codex CLI to use the refreshed install surface.",
config_path.display()
);
} else if !created && before.status == "conflict" {
state.status = "conflict";
state.action_status = "preserved";
state.backup_status = "not-required";
state.backup_source_path = None;
state.backup_path = None;
state.setup_migration_deltas =
setup_migration_delta_payload(&before.value, &state.value, state.action_status);
state.summary = format!(
"Canonical CCC config at {} was preserved because a legacy migration source conflicts with it; restart Codex CLI if setup changed registration or skill files.",
config_path.display()
);
} else if !created {
state.status = "canonical-current";
state.action_status = "preserved";
state.backup_status = "not-required";
state.backup_source_path = None;
state.backup_path = None;
state.setup_migration_deltas =
setup_migration_delta_payload(&before.value, &state.value, state.action_status);
state.summary = format!(
"Canonical CCC config at {} was already current; restart Codex CLI if setup changed registration or skill files.",
config_path.display()
);
}
}
Ok((created_path, created, state))
}
pub(crate) fn ensure_ccc_config_file() -> io::Result<(PathBuf, bool)> {
let config_path = resolve_shared_config_path();
let legacy_toml_path = resolve_legacy_shared_toml_config_path();
let legacy_json_path = resolve_legacy_shared_json_config_path();
ensure_ccc_config_file_at(&config_path, &legacy_toml_path, &legacy_json_path)
}
pub(crate) fn ensure_ccc_config_file_with_state(
) -> io::Result<(PathBuf, bool, CccConfigInstallState)> {
let config_path = resolve_shared_config_path();
let legacy_toml_path = resolve_legacy_shared_toml_config_path();
let legacy_json_path = resolve_legacy_shared_json_config_path();
ensure_ccc_config_file_at_with_state(&config_path, &legacy_toml_path, &legacy_json_path)
}