mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-11 12:03:31 +08:00
feat: add ecc2 legacy migration plan
This commit is contained in:
299
ecc2/src/main.rs
299
ecc2/src/main.rs
@@ -584,6 +584,18 @@ enum MigrationCommands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
|
/// Generate an actionable ECC2 migration plan from a legacy workspace audit
|
||||||
|
Plan {
|
||||||
|
/// Path to the legacy Hermes/OpenClaw workspace root
|
||||||
|
#[arg(long)]
|
||||||
|
source: PathBuf,
|
||||||
|
/// Write the plan to a file instead of stdout
|
||||||
|
#[arg(long)]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
/// Emit machine-readable JSON instead of the human summary
|
||||||
|
#[arg(long)]
|
||||||
|
json: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Subcommand, Debug)]
|
#[derive(clap::Subcommand, Debug)]
|
||||||
@@ -914,6 +926,26 @@ struct LegacyMigrationAuditReport {
|
|||||||
artifacts: Vec<LegacyMigrationArtifact>,
|
artifacts: Vec<LegacyMigrationArtifact>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
struct LegacyMigrationPlanStep {
|
||||||
|
category: String,
|
||||||
|
readiness: LegacyMigrationReadiness,
|
||||||
|
title: String,
|
||||||
|
target_surface: String,
|
||||||
|
source_paths: Vec<String>,
|
||||||
|
command_snippets: Vec<String>,
|
||||||
|
config_snippets: Vec<String>,
|
||||||
|
notes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
struct LegacyMigrationPlanReport {
|
||||||
|
source: String,
|
||||||
|
generated_at: String,
|
||||||
|
audit_summary: LegacyMigrationAuditSummary,
|
||||||
|
steps: Vec<LegacyMigrationPlanStep>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
struct RemoteDispatchHttpRequest {
|
struct RemoteDispatchHttpRequest {
|
||||||
task: String,
|
task: String,
|
||||||
@@ -1650,6 +1682,25 @@ async fn main() -> Result<()> {
|
|||||||
println!("{}", format_legacy_migration_audit_human(&report));
|
println!("{}", format_legacy_migration_audit_human(&report));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
MigrationCommands::Plan {
|
||||||
|
source,
|
||||||
|
output,
|
||||||
|
json,
|
||||||
|
} => {
|
||||||
|
let audit = build_legacy_migration_audit_report(&source)?;
|
||||||
|
let plan = build_legacy_migration_plan_report(&audit);
|
||||||
|
let rendered = if json {
|
||||||
|
serde_json::to_string_pretty(&plan)?
|
||||||
|
} else {
|
||||||
|
format_legacy_migration_plan_human(&plan)
|
||||||
|
};
|
||||||
|
if let Some(path) = output {
|
||||||
|
std::fs::write(&path, &rendered)?;
|
||||||
|
println!("Migration plan written to {}", path.display());
|
||||||
|
} else {
|
||||||
|
println!("{rendered}");
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Some(Commands::Graph { command }) => match command {
|
Some(Commands::Graph { command }) => match command {
|
||||||
GraphCommands::AddEntity {
|
GraphCommands::AddEntity {
|
||||||
@@ -4797,6 +4848,145 @@ fn build_legacy_migration_next_steps(artifacts: &[LegacyMigrationArtifact]) -> V
|
|||||||
steps
|
steps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_legacy_migration_plan_report(
|
||||||
|
audit: &LegacyMigrationAuditReport,
|
||||||
|
) -> LegacyMigrationPlanReport {
|
||||||
|
let mut steps = Vec::new();
|
||||||
|
|
||||||
|
for artifact in &audit.artifacts {
|
||||||
|
let step = match artifact.category.as_str() {
|
||||||
|
"scheduler" => LegacyMigrationPlanStep {
|
||||||
|
category: artifact.category.clone(),
|
||||||
|
readiness: artifact.readiness,
|
||||||
|
title: "Recreate Hermes/OpenClaw recurring jobs in ECC2 scheduler".to_string(),
|
||||||
|
target_surface: "ECC2 scheduler".to_string(),
|
||||||
|
source_paths: artifact.source_paths.clone(),
|
||||||
|
command_snippets: vec![
|
||||||
|
"ecc schedule add --cron \"<legacy-cron>\" --task \"Translate legacy recurring job from cron/scheduler.py\"".to_string(),
|
||||||
|
"ecc schedule list".to_string(),
|
||||||
|
"ecc daemon".to_string(),
|
||||||
|
],
|
||||||
|
config_snippets: Vec::new(),
|
||||||
|
notes: artifact.notes.clone(),
|
||||||
|
},
|
||||||
|
"gateway_dispatch" => LegacyMigrationPlanStep {
|
||||||
|
category: artifact.category.clone(),
|
||||||
|
readiness: artifact.readiness,
|
||||||
|
title: "Replace legacy gateway intake with ECC2 remote dispatch".to_string(),
|
||||||
|
target_surface: "ECC2 remote dispatch".to_string(),
|
||||||
|
source_paths: artifact.source_paths.clone(),
|
||||||
|
command_snippets: vec![
|
||||||
|
"ecc remote serve --bind 127.0.0.1:8787 --token <token>".to_string(),
|
||||||
|
"ecc remote add --task \"Translate legacy dispatch workflow\"".to_string(),
|
||||||
|
"ecc remote computer-use --goal \"Translate legacy browser/operator flow\"".to_string(),
|
||||||
|
],
|
||||||
|
config_snippets: Vec::new(),
|
||||||
|
notes: artifact.notes.clone(),
|
||||||
|
},
|
||||||
|
"memory_tool" => LegacyMigrationPlanStep {
|
||||||
|
category: artifact.category.clone(),
|
||||||
|
readiness: artifact.readiness,
|
||||||
|
title: "Port legacy memory tool usage to ECC2 deep memory".to_string(),
|
||||||
|
target_surface: "ECC2 context graph".to_string(),
|
||||||
|
source_paths: artifact.source_paths.clone(),
|
||||||
|
command_snippets: vec![
|
||||||
|
"ecc graph add-observation --entity-id <id> --type migration_note --summary \"Imported legacy memory pattern\"".to_string(),
|
||||||
|
"ecc graph recall \"<query>\"".to_string(),
|
||||||
|
"ecc graph connectors".to_string(),
|
||||||
|
],
|
||||||
|
config_snippets: Vec::new(),
|
||||||
|
notes: artifact.notes.clone(),
|
||||||
|
},
|
||||||
|
"workspace_memory" => LegacyMigrationPlanStep {
|
||||||
|
category: artifact.category.clone(),
|
||||||
|
readiness: artifact.readiness,
|
||||||
|
title: "Import sanitized workspace memory through ECC2 connectors".to_string(),
|
||||||
|
target_surface: "ECC2 memory connectors".to_string(),
|
||||||
|
source_paths: artifact.source_paths.clone(),
|
||||||
|
command_snippets: vec![
|
||||||
|
"ecc graph connector-sync hermes_workspace".to_string(),
|
||||||
|
"ecc graph recall \"<query>\"".to_string(),
|
||||||
|
],
|
||||||
|
config_snippets: vec![format!(
|
||||||
|
"[memory_connectors.hermes_workspace]\nkind = \"markdown_directory\"\npath = \"{}\"\nrecurse = true\ndefault_entity_type = \"legacy_workspace_note\"\ndefault_observation_type = \"legacy_workspace_memory\"",
|
||||||
|
Path::new(&audit.source).join("workspace").display()
|
||||||
|
)],
|
||||||
|
notes: artifact.notes.clone(),
|
||||||
|
},
|
||||||
|
"skills" => LegacyMigrationPlanStep {
|
||||||
|
category: artifact.category.clone(),
|
||||||
|
readiness: artifact.readiness,
|
||||||
|
title: "Translate reusable legacy skills into ECC-native surfaces".to_string(),
|
||||||
|
target_surface: "ECC skills / orchestration templates".to_string(),
|
||||||
|
source_paths: artifact.source_paths.clone(),
|
||||||
|
command_snippets: vec![
|
||||||
|
"ecc template <template-name> --task \"<translated workflow goal>\"".to_string(),
|
||||||
|
],
|
||||||
|
config_snippets: vec![
|
||||||
|
"[orchestration_templates.legacy_workflow]\nproject = \"legacy-migration\"\ntask_group = \"legacy workflow\"\nagent = \"claude\"\nworktree = false\n\n[[orchestration_templates.legacy_workflow.steps]]\nname = \"operator\"\ntask = \"Translate and run the legacy workflow for {{task}}\"".to_string(),
|
||||||
|
],
|
||||||
|
notes: artifact.notes.clone(),
|
||||||
|
},
|
||||||
|
"tools" => LegacyMigrationPlanStep {
|
||||||
|
category: artifact.category.clone(),
|
||||||
|
readiness: artifact.readiness,
|
||||||
|
title: "Rebuild valuable legacy tools as ECC agents, hooks, commands, or harness runners".to_string(),
|
||||||
|
target_surface: "ECC agents / hooks / commands / harness runners".to_string(),
|
||||||
|
source_paths: artifact.source_paths.clone(),
|
||||||
|
command_snippets: vec![
|
||||||
|
"ecc start --task \"Rebuild one legacy tool as an ECC-native command or hook\"".to_string(),
|
||||||
|
],
|
||||||
|
config_snippets: vec![
|
||||||
|
"[harness_runners.legacy-runner]\nprogram = \"<runner-binary>\"\nbase_args = []\nproject_markers = [\".legacy-runner\"]".to_string(),
|
||||||
|
],
|
||||||
|
notes: artifact.notes.clone(),
|
||||||
|
},
|
||||||
|
"plugins" => LegacyMigrationPlanStep {
|
||||||
|
category: artifact.category.clone(),
|
||||||
|
readiness: artifact.readiness,
|
||||||
|
title: "Translate legacy bridge plugins into ECC-native automation".to_string(),
|
||||||
|
target_surface: "ECC hooks / commands / skills".to_string(),
|
||||||
|
source_paths: artifact.source_paths.clone(),
|
||||||
|
command_snippets: vec![
|
||||||
|
"ecc start --task \"Port one bridge plugin behavior into an ECC hook or command\"".to_string(),
|
||||||
|
],
|
||||||
|
config_snippets: Vec::new(),
|
||||||
|
notes: artifact.notes.clone(),
|
||||||
|
},
|
||||||
|
"env_services" => LegacyMigrationPlanStep {
|
||||||
|
category: artifact.category.clone(),
|
||||||
|
readiness: artifact.readiness,
|
||||||
|
title: "Reconfigure local auth and connectors without importing secrets".to_string(),
|
||||||
|
target_surface: "Claude connectors / MCP / local API key setup".to_string(),
|
||||||
|
source_paths: artifact.source_paths.clone(),
|
||||||
|
command_snippets: Vec::new(),
|
||||||
|
config_snippets: vec![
|
||||||
|
"# Re-enter connector auth locally; do not copy legacy secrets into ECC2.\n# Typical targets: Google Drive OAuth, GitHub, Stripe, Linear, browser creds.".to_string(),
|
||||||
|
],
|
||||||
|
notes: artifact.notes.clone(),
|
||||||
|
},
|
||||||
|
_ => LegacyMigrationPlanStep {
|
||||||
|
category: artifact.category.clone(),
|
||||||
|
readiness: artifact.readiness,
|
||||||
|
title: format!("Review legacy {} surface", artifact.category),
|
||||||
|
target_surface: "Manual ECC2 translation".to_string(),
|
||||||
|
source_paths: artifact.source_paths.clone(),
|
||||||
|
command_snippets: Vec::new(),
|
||||||
|
config_snippets: Vec::new(),
|
||||||
|
notes: artifact.notes.clone(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
steps.push(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
LegacyMigrationPlanReport {
|
||||||
|
source: audit.source.clone(),
|
||||||
|
generated_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
audit_summary: audit.summary.clone(),
|
||||||
|
steps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn format_legacy_migration_audit_human(report: &LegacyMigrationAuditReport) -> String {
|
fn format_legacy_migration_audit_human(report: &LegacyMigrationAuditReport) -> String {
|
||||||
let mut lines = vec![
|
let mut lines = vec![
|
||||||
format!("Legacy migration audit: {}", report.source),
|
format!("Legacy migration audit: {}", report.source),
|
||||||
@@ -4855,6 +5045,53 @@ fn format_legacy_migration_readiness(readiness: LegacyMigrationReadiness) -> &'s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_legacy_migration_plan_human(report: &LegacyMigrationPlanReport) -> String {
|
||||||
|
let mut lines = vec![
|
||||||
|
format!("Legacy migration plan: {}", report.source),
|
||||||
|
format!("Generated at: {}", report.generated_at),
|
||||||
|
format!(
|
||||||
|
"Audit summary: {} categories | ready now {} | manual translation {} | local auth {}",
|
||||||
|
report.audit_summary.artifact_categories_detected,
|
||||||
|
report.audit_summary.ready_now_categories,
|
||||||
|
report.audit_summary.manual_translation_categories,
|
||||||
|
report.audit_summary.local_auth_required_categories
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
if report.steps.is_empty() {
|
||||||
|
lines.push("No migration steps generated.".to_string());
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(String::new());
|
||||||
|
lines.push("Plan".to_string());
|
||||||
|
for step in &report.steps {
|
||||||
|
lines.push(format!(
|
||||||
|
"- {} [{}] -> {}",
|
||||||
|
step.title,
|
||||||
|
format_legacy_migration_readiness(step.readiness),
|
||||||
|
step.target_surface
|
||||||
|
));
|
||||||
|
if !step.source_paths.is_empty() {
|
||||||
|
lines.push(format!(" sources {}", step.source_paths.join(", ")));
|
||||||
|
}
|
||||||
|
for command in &step.command_snippets {
|
||||||
|
lines.push(format!(" command {}", command));
|
||||||
|
}
|
||||||
|
for snippet in &step.config_snippets {
|
||||||
|
lines.push(" config".to_string());
|
||||||
|
for line in snippet.lines() {
|
||||||
|
lines.push(format!(" {}", line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for note in &step.notes {
|
||||||
|
lines.push(format!(" note {}", note));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
fn format_graph_recall_human(
|
fn format_graph_recall_human(
|
||||||
entries: &[session::ContextGraphRecallEntry],
|
entries: &[session::ContextGraphRecallEntry],
|
||||||
session_id: Option<&str>,
|
session_id: Option<&str>,
|
||||||
@@ -7301,6 +7538,36 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_parses_migrate_plan_command() {
|
||||||
|
let cli = Cli::try_parse_from([
|
||||||
|
"ecc",
|
||||||
|
"migrate",
|
||||||
|
"plan",
|
||||||
|
"--source",
|
||||||
|
"/tmp/hermes",
|
||||||
|
"--output",
|
||||||
|
"/tmp/plan.md",
|
||||||
|
])
|
||||||
|
.expect("migrate plan should parse");
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Some(Commands::Migrate {
|
||||||
|
command:
|
||||||
|
MigrationCommands::Plan {
|
||||||
|
source,
|
||||||
|
output,
|
||||||
|
json,
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
assert_eq!(source, PathBuf::from("/tmp/hermes"));
|
||||||
|
assert_eq!(output, Some(PathBuf::from("/tmp/plan.md")));
|
||||||
|
assert!(!json);
|
||||||
|
}
|
||||||
|
_ => panic!("expected migrate plan subcommand"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn legacy_migration_audit_report_maps_detected_artifacts() -> Result<()> {
|
fn legacy_migration_audit_report_maps_detected_artifacts() -> Result<()> {
|
||||||
let tempdir = TestDir::new("legacy-migration-audit")?;
|
let tempdir = TestDir::new("legacy-migration-audit")?;
|
||||||
@@ -7368,6 +7635,38 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_migration_plan_report_generates_workspace_connector_step() -> Result<()> {
|
||||||
|
let tempdir = TestDir::new("legacy-migration-plan")?;
|
||||||
|
let root = tempdir.path();
|
||||||
|
fs::create_dir_all(root.join("workspace/notes"))?;
|
||||||
|
fs::write(root.join("config.yaml"), "model: claude\n")?;
|
||||||
|
fs::write(root.join("workspace/notes/recovery.md"), "# recovery\n")?;
|
||||||
|
|
||||||
|
let audit = build_legacy_migration_audit_report(root)?;
|
||||||
|
let plan = build_legacy_migration_plan_report(&audit);
|
||||||
|
|
||||||
|
let workspace_step = plan
|
||||||
|
.steps
|
||||||
|
.iter()
|
||||||
|
.find(|step| step.category == "workspace_memory")
|
||||||
|
.expect("workspace memory step");
|
||||||
|
assert_eq!(workspace_step.readiness, LegacyMigrationReadiness::ReadyNow);
|
||||||
|
assert!(workspace_step
|
||||||
|
.config_snippets
|
||||||
|
.iter()
|
||||||
|
.any(|snippet| snippet.contains("[memory_connectors.hermes_workspace]")));
|
||||||
|
assert!(workspace_step
|
||||||
|
.command_snippets
|
||||||
|
.contains(&"ecc graph connector-sync hermes_workspace".to_string()));
|
||||||
|
|
||||||
|
let rendered = format_legacy_migration_plan_human(&plan);
|
||||||
|
assert!(rendered.contains("Legacy migration plan"));
|
||||||
|
assert!(rendered.contains("Import sanitized workspace memory through ECC2 connectors"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_decisions_human_renders_details() {
|
fn format_decisions_human_renders_details() {
|
||||||
let text = format_decisions_human(
|
let text = format_decisions_human(
|
||||||
|
|||||||
Reference in New Issue
Block a user