use regex::Regex;
use super::{MigrateError, MigrationResult};
static TUI_HEADER_RE: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"(?m)^[ \t]*\[tui\][ \t]*(?:#[^\r\n]*)?\r?\n").expect("static pattern")
});
pub fn migrate_tui_delights(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if !toml_src.contains("[tui]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let already_present = toml_src.lines().any(|l| {
let t = l.trim().trim_start_matches('#').trim();
t == "[tui.delights]" || t.starts_with("[tui.delights]")
});
if already_present {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let owned;
let src = if toml_src.ends_with('\n') {
toml_src
} else {
owned = format!("{toml_src}\n");
&owned
};
if !TUI_HEADER_RE.is_match(src) {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let advisory = "\n# [tui.delights] — micro-delight toggles (all default true, #5104).\n\
# motion = off acts as a master kill-switch regardless of individual settings.\n\
# [tui.delights]\n\
# stream_metrics = true # tok/s during streaming + TTFT after turn in status bar\n\
# toasts = true # ephemeral overlay notifications (theme switch, copy, etc.)\n\
# completion_flash = true # accent tint on a finished tool group for ~400 ms\n\
# smooth_scroll = true # eased multi-frame interpolation on page-up / page-down\n\
# splash_shimmer = true # one-shot gradient sweep across the wordmark at startup\n";
let output = TUI_HEADER_RE
.replacen(src, 1, |caps: ®ex::Captures| {
format!("{}{advisory}", &caps[0])
})
.into_owned();
let changed = output != toml_src;
let changed_count = usize::from(changed);
Ok(MigrationResult {
output,
changed_count,
sections_changed: if changed {
vec!["tui.delights".to_owned()]
} else {
Vec::new()
},
})
}
pub fn migrate_tui_mouse(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if !toml_src.contains("[tui]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let already_present = toml_src.lines().any(|l| {
let t = l.trim().trim_start_matches('#').trim();
t.starts_with("mouse")
});
if already_present {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let owned;
let src = if toml_src.ends_with('\n') {
toml_src
} else {
owned = format!("{toml_src}\n");
&owned
};
if !TUI_HEADER_RE.is_match(src) {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let insert =
"# mouse = false # opt-in mouse capture: wheel scrolls, clicks focus panels (#5103)\n";
let output = TUI_HEADER_RE
.replacen(src, 1, |caps: ®ex::Captures| {
format!("{}{insert}", &caps[0])
})
.into_owned();
let changed = output != toml_src;
let changed_count = usize::from(changed);
Ok(MigrationResult {
output,
changed_count,
sections_changed: if changed {
vec!["tui".to_owned()]
} else {
Vec::new()
},
})
}
pub fn migrate_compression_predictor_config(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
let has_active = toml_src.contains("[memory.compression.predictor]");
let has_commented = toml_src.contains("# [memory.compression.predictor]");
if !has_active && !has_commented {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let mut output_lines: Vec<&str> = Vec::new();
let mut in_predictor = false;
for line in toml_src.lines() {
let trimmed = line.trim();
if trimmed == "[memory.compression.predictor]"
|| trimmed == "# [memory.compression.predictor]"
{
in_predictor = true;
continue;
}
if in_predictor && trimmed.starts_with('[') && !trimmed.starts_with("# [") {
in_predictor = false;
}
if !in_predictor {
output_lines.push(line);
}
}
let mut output = output_lines.join("\n");
if toml_src.ends_with('\n') {
output.push('\n');
}
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.compression.predictor".to_owned()],
})
}
pub fn migrate_microcompact_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("[memory.microcompact]") || toml_src.contains("# [memory.microcompact]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
if !doc.contains_key("memory") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# Time-based microcompact (#2699). Strips stale low-value tool outputs after idle.\n\
# [memory.microcompact]\n\
# enabled = false\n\
# gap_threshold_minutes = 60 # idle gap before clearing stale outputs\n\
# keep_recent = 3 # always keep this many recent outputs intact\n";
let raw = doc.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.microcompact".to_owned()],
})
}
pub fn migrate_autodream_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("[memory.autodream]") || toml_src.contains("# [memory.autodream]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
if !doc.contains_key("memory") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# autoDream background memory consolidation (#2697). Disabled by default.\n\
# [memory.autodream]\n\
# enabled = false\n\
# min_sessions = 5 # sessions since last consolidation\n\
# min_hours = 8 # hours since last consolidation\n\
# consolidation_provider = \"\" # provider name from [[llm.providers]]; empty = primary\n\
# max_iterations = 5\n";
let raw = doc.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.autodream".to_owned()],
})
}
pub fn migrate_magic_docs_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
use toml_edit::{Item, Table};
let mut doc = toml_src.parse::<toml_edit::DocumentMut>()?;
if doc.contains_key("magic_docs") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
doc.insert("magic_docs", Item::Table(Table::new()));
let comment = "# MagicDocs auto-maintained markdown (#2702). Disabled by default.\n\
# [magic_docs]\n\
# enabled = false\n\
# min_turns_between_updates = 10\n\
# update_provider = \"\" # provider name from [[llm.providers]]; empty = primary\n\
# max_iterations = 3\n";
doc.remove("magic_docs");
let raw = doc.to_string();
let output = format!("{raw}\n{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["magic_docs".to_owned()],
})
}
pub fn migrate_orchestration_persistence(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("persistence_enabled") || toml_src.contains("# persistence_enabled") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
if !toml_src.contains("[orchestration]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "# persistence_enabled = true \
# persist task graphs to SQLite after each tick; enables `/plan resume <id>` (#3107)\n";
let output = toml_src.replacen(
"[orchestration]\n",
&format!("[orchestration]\n{comment}"),
1,
);
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["orchestration.persistence_enabled".to_owned()],
})
}
pub fn migrate_goals_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("[goals]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# Long-horizon goal lifecycle tracking (#3567).\n\
# [goals]\n\
# enabled = false\n\
# inject_into_system_prompt = true\n\
# max_text_chars = 2000\n\
# max_history = 50\n";
Ok(MigrationResult {
output: format!("{toml_src}{comment}"),
changed_count: 1,
sections_changed: vec!["goals".to_owned()],
})
}
pub fn migrate_caveman_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("[caveman]") || toml_src.contains("# [caveman]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [caveman] — ultra-compressed telegraphic output mode (#4985).\n\
# Toggle at runtime with /caveman [on|off] or via the bundled caveman skill.\n\
# [caveman]\n\
# default_on = false\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["caveman".to_owned()],
})
}
pub fn migrate_deep_link_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("[deep_link]") || toml_src.contains("# [deep_link]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [deep_link] — zeph:// URI scheme configuration (spec-066, #5011).\n\
# Requires the `deep-link` Cargo feature to be active.\n\
# [deep_link]\n\
# confirm_before_prompt = true # require y/N before injecting prompt (secure default)\n\
# allowed_cwd_roots = [] # restrict cwd to these prefixes; empty = any non-denylisted path\n\
# prefer_acp = \"never\" # v1 only: \"never\"; \"auto\"/\"always\" reserved for v2\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["deep_link".to_owned()],
})
}
pub fn migrate_five_signal_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("[memory.five_signal]") || toml_src.contains("# [memory.five_signal]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
if !doc.contains_key("memory") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# Five-signal SYNAPSE retrieval (#4374). Disabled by default.\n\
# [memory.five_signal]\n\
# enabled = false\n\
# w_recency = 0.35\n\
# w_relevance = 0.35\n\
# w_frequency = 0.15\n\
# w_causal = 0.10\n\
# w_novelty = 0.05\n\
# causal_bfs_max_depth = 10\n\
# neutral_causal_distance = 5\n\
# novelty_decay_rate = 0.1\n\
#\n\
# [memory.five_signal.consolidation_daemon]\n\
# enabled = false\n\
# interval_seconds = 7200\n\
# batch_size = 500\n\
# promotion_score_threshold = 0.70\n\
# demotion_score_threshold = 0.20\n\
# top_k_per_run = 500\n";
let raw = doc.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.five_signal".to_owned()],
})
}
pub fn migrate_knowledge_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("[knowledge]") || toml_src.contains("# [knowledge]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# Knowledge-ingest subsystem (spec-067, #5017). All defaults shown.\n\
# [knowledge]\n\
# ingest_provider = \"\" # provider from [[llm.providers]]; empty = primary (Phase 2 graph)\n\
# concurrency = 3 # max parallel extract tasks (Phase 2)\n\
# max_documents = 0 # 0 = unlimited; CLI --max-documents overrides\n\
# recall_include_imported = true # include imported rows in semantic recall\n\
# transcript_scope = \"current-project\" # INV-6: only current-project supported in Phase 1\n";
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
let raw = doc.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["knowledge".to_owned()],
})
}
static TUI_THEME_HEADER_RE: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"(?m)^[ \t]*\[tui\.theme\][ \t]*(?:#[^\r\n]*)?\r?\n").expect("static pattern")
});
pub fn migrate_tui_theme_defaults(toml_src: &str) -> Result<MigrationResult, MigrateError> {
let key_in_tui_theme = |key: &str| {
let mut in_tui_theme = false;
toml_src.lines().any(|l| {
let t = l.trim();
if !t.starts_with('#') && t.starts_with('[') {
in_tui_theme = t == "[tui.theme]";
return false;
}
if !in_tui_theme {
return false;
}
let body = t.trim_start_matches('#').trim();
let lhs = body.split('=').next().unwrap_or("").trim();
lhs == key
})
};
let has_name = key_in_tui_theme("name");
let has_color_mode = key_in_tui_theme("color_mode");
let has_section = toml_src.contains("[tui.theme]");
if !has_section || (has_name && has_color_mode) {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let owned;
let src = if toml_src.ends_with('\n') {
toml_src
} else {
owned = format!("{toml_src}\n");
&owned
};
if !TUI_THEME_HEADER_RE.is_match(src) {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let mut insert = String::new();
if !has_name {
insert
.push_str("name = \"zephyr\" # built-in preset; see /theme for alternatives\n");
}
if !has_color_mode {
insert.push_str("color_mode = \"auto\" # auto | truecolor | ansi256 | ansi16 | never\n");
}
let output = TUI_THEME_HEADER_RE
.replacen(src, 1, |caps: ®ex::Captures| {
format!("{}{insert}", &caps[0])
})
.into_owned();
let changed = output != toml_src;
let changed_count = usize::from(changed);
Ok(MigrationResult {
output,
changed_count,
sections_changed: if changed {
vec!["tui.theme".to_owned()]
} else {
Vec::new()
},
})
}
pub fn migrate_tui_theme_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
let in_tui_section = {
let mut in_section = false;
toml_src.lines().any(|l| {
let t = l.trim();
if !t.starts_with('#') && t.starts_with('[') && !t.starts_with("[[") {
in_section = t == "[tui]";
return false;
}
if t.starts_with('#') {
let inner = t.trim_start_matches('#').trim();
if inner.starts_with('[') {
in_section = false;
return false;
}
return in_section && (inner == "[tui.theme]" || inner.starts_with("tui.theme"));
}
in_section && (t == "[tui.theme]" || t.starts_with("tui.theme"))
})
};
if in_tui_section || toml_src.contains("[tui.theme]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
let raw = doc.to_string();
let comment = "\n# [tui.theme] — TUI visual theme (Theme System 2.0, #5087).\n\
# name sets the colour palette. Built-in presets: classic, zephyr, zephyr-light,\n\
# high-contrast, catppuccin-mocha, gruvbox-dark, solarized-dark.\n\
# Custom palettes: drop a TOML file in ~/.config/zeph/themes/<name>.toml.\n\
# [tui.theme]\n\
# name = \"zephyr\" # default: zephyr (new default since 2.0; use \"classic\" for legacy look)\n\
# color_mode = \"auto\" # auto | truecolor | ansi256 | ansi16 | never\n";
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["tui.theme".to_owned()],
})
}
pub fn migrate_orchestration_asset_sensitivity(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("default_asset_sensitivity")
|| toml_src.contains("# default_asset_sensitivity")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
if !toml_src.contains("[orchestration]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "# default_asset_sensitivity = \"public\" \
# advisory asset sensitivity: public | internal | confidential (spec-068, #3934)\n";
let output = toml_src.replacen(
"[orchestration]\n",
&format!("[orchestration]\n{comment}"),
1,
);
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["orchestration.default_asset_sensitivity".to_owned()],
})
}