diff --git a/codex-rs/tui/prompt_for_migrate_command.md b/codex-rs/tui/prompt_for_migrate_command.md index 62d46f7292..f47a3f375c 100644 --- a/codex-rs/tui/prompt_for_migrate_command.md +++ b/codex-rs/tui/prompt_for_migrate_command.md @@ -1,12 +1,23 @@ -You are the lead engineer orchestrating a large-scale migration. -Before writing any code, inspect the repository layout, dependencies, infrastructure, and release process to understand the scope of the migration. +You are the principal migration showrunner responsible for multi-quarter, cross-team transformations. +Before proposing any code or tooling changes, map out the repo layout, data stores, release cadence, and operational constraints so the migration plan is grounded in reality. -Deliverable +## Mission objectives +- Produce an incremental, numbered plan that safely delivers the migration in phases. +- Surface dependencies, data/backfill steps, validation and rollback strategies, and required approvals. +- Identify which efforts can be parallelized by multiple agents or teams and how they will sync context. +- Explicitly call out observability, customer impact, compliance, and communication touchpoints. -- Produce a numbered migration plan (1., 2., 3., …) covering every task that must be completed. -- Each item should contain: a short objective, the concrete changes required, dependencies or prerequisites, and the validation strategy. -- Highlight risks, data migrations, rollout/rollback considerations, and any coordination with other teams. -- Call out tasks that can run in parallel vs. those that must happen sequentially. -- Keep the tone directive and actionable so that another engineer could execute the plan. +## Deliverables +1. **Mission Brief** – concise summary of the current vs. target state and the success criteria. +2. **Discovery + Unknowns** – what must be inspected or confirmed before execution (specific files, systems, SMEs). +3. **Readiness & Risk Radar** – gating checks, risk matrix, and mitigation ideas. +4. **Phased Execution Plan** – numbered steps (1., 2., 3., …). Each step must include objective, concrete changes, owners/skills, dependencies, blast radius, validation/rollback, and artifacts to produce. +5. **Parallel Work Grid** – table of workstreams that can run concurrently, including prerequisites, shared learnings, and how progress is published so agents can learn from each other. +6. **Publishing & Feedback Loop** – instructions for how the canonical plan and async updates should be maintained in the migration workspace, plus how agents signal completion or ask for help. +7. **Next Questions** – anything still unknown that would block progress. -When you need more context, describe exactly what information you require from the user or which files should be inspected next. +## Execution guidance +- Treat the migration as a program: outline sequencing, checkpoints, and explicit handoffs. +- When highlighting parallelizable work, explain how agents reuse each other's findings (artifacts, logs, dashboards, test outputs). +- Always mention data migrations, schema contracts, backfills, and customer rollout/rollback mechanics. +- If information is missing, specify exactly what to inspect or who to ask before proceeding. diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 1d2c9690d5..f60a2c85b0 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -369,6 +369,9 @@ impl App { AppEvent::OpenFeedbackConsent { category } => { self.chat_widget.open_feedback_consent(category); } + AppEvent::StartMigrationWorkflow { label } => { + self.chat_widget.start_migration_workflow(label); + } AppEvent::ShowWindowsAutoModeInstructions => { self.chat_widget.open_windows_auto_mode_instructions(); } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 514e234743..a719c4a57e 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -115,6 +115,11 @@ pub(crate) enum AppEvent { OpenFeedbackConsent { category: FeedbackCategory, }, + + /// Kick off a /migrate workflow after collecting the migration summary. + StartMigrationWorkflow { + label: String, + }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 8fc538e16b..877d56f0cd 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -52,6 +52,7 @@ use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use crossterm::event::KeyModifiers; +use pathdiff::diff_paths; use rand::Rng; use ratatui::buffer::Buffer; use ratatui::layout::Rect; @@ -86,6 +87,7 @@ use crate::history_cell::AgentMessageCell; use crate::history_cell::HistoryCell; use crate::history_cell::McpToolCallCell; use crate::markdown::append_markdown; +use crate::migration::prepare_workspace; #[cfg(target_os = "windows")] use crate::onboarding::WSL_INSTRUCTIONS; use crate::render::Insets; @@ -1235,8 +1237,7 @@ impl ChatWidget { self.submit_user_message(INIT_PROMPT.to_string().into()); } SlashCommand::Migrate => { - const MIGRATE_PROMPT: &str = include_str!("../prompt_for_migrate_command.md"); - self.submit_user_message(MIGRATE_PROMPT.to_string().into()); + self.open_migration_prompt(); } SlashCommand::Compact => { self.clear_token_usage(); @@ -2426,6 +2427,68 @@ impl ChatWidget { self.bottom_pane.show_view(Box::new(view)); } + pub(crate) fn open_migration_prompt(&mut self) { + let tx = self.app_event_tx.clone(); + let view = CustomPromptView::new( + "Plan a migration".to_string(), + "Example: Gradually replace the billing monolith with Rust services".to_string(), + Some( + "Codex will create migration_ artifacts before generating the plan." + .to_string(), + ), + Box::new(move |summary: String| { + let trimmed = summary.trim(); + if trimmed.is_empty() { + return; + } + tx.send(AppEvent::StartMigrationWorkflow { + label: trimmed.to_string(), + }); + }), + ); + self.bottom_pane.show_view(Box::new(view)); + } + + pub(crate) fn start_migration_workflow(&mut self, label: String) { + let trimmed = label.trim(); + if trimmed.is_empty() { + self.add_error_message( + "Please describe the migration before running /migrate.".to_string(), + ); + return; + } + let workspace = match prepare_workspace(&self.config.cwd, trimmed) { + Ok(ws) => ws, + Err(err) => { + self.add_error_message(format!("Failed to set up migration workspace: {err}")); + return; + } + }; + let workspace_rel = self.relative_to_cwd(&workspace.dir_path); + let plan_rel = self.relative_to_cwd(&workspace.plan_path); + let log_rel = self.relative_to_cwd(&workspace.progress_log_path); + let info = format!( + "Created `{workspace_rel}` with `{plan_rel}` for the plan and `{log_rel}` for async updates." + ); + self.add_info_message( + info, + Some("Keep the plan and progress log updated as tasks complete.".to_string()), + ); + + const MIGRATE_PROMPT_TEXT: &str = include_str!("../prompt_for_migrate_command.md"); + let prompt = format!( + "{MIGRATE_PROMPT_TEXT}\n\n### Workspace context\n- Migration codename: {trimmed}\n- Shared workspace: `{workspace_rel}`\n- Canonical plan file: `{plan_rel}`\n- Progress log for multi-agent updates: `{log_rel}`\n\nPopulate `{plan_rel}` with the plan you produce and mirror incremental updates in `{log_rel}` so parallel workstreams stay synchronized.", + ); + self.submit_user_message(prompt.into()); + } + + fn relative_to_cwd(&self, path: &Path) -> String { + diff_paths(path, &self.config.cwd) + .unwrap_or_else(|| path.to_path_buf()) + .display() + .to_string() + } + pub(crate) fn token_usage(&self) -> TokenUsage { self.token_info .as_ref() diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 99312ec198..d211c1bf6e 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -54,6 +54,7 @@ pub mod live_wrap; mod markdown; mod markdown_render; mod markdown_stream; +mod migration; pub mod onboarding; mod pager_overlay; pub mod public_widgets; diff --git a/codex-rs/tui/src/migration.rs b/codex-rs/tui/src/migration.rs new file mode 100644 index 0000000000..cb6e244504 --- /dev/null +++ b/codex-rs/tui/src/migration.rs @@ -0,0 +1,168 @@ +use chrono::Local; +use std::fs; +use std::io; +use std::path::Path; +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub(crate) struct MigrationWorkspace { + pub dir_path: PathBuf, + pub plan_path: PathBuf, + pub progress_log_path: PathBuf, +} + +pub(crate) fn prepare_workspace(root: &Path, label: &str) -> io::Result { + let trimmed = label.trim(); + let now = Local::now(); + let slug = slugify_label(trimmed); + let base_dir_name = if slug.is_empty() { + format!("migration_{}", now.format("%Y%m%d-%H%M%S")) + } else { + format!("migration_{slug}") + }; + let (dir_name, dir_path) = next_available_dir(root, &base_dir_name); + fs::create_dir_all(&dir_path)?; + let created_label = now.format("%Y-%m-%d %H:%M:%S %Z").to_string(); + + let plan_path = dir_path.join("plan.md"); + if !plan_path.exists() { + fs::write( + &plan_path, + initial_plan_template(trimmed, &dir_name, &created_label), + )?; + } + + let progress_log_path = dir_path.join("progress_log.md"); + if !progress_log_path.exists() { + fs::write( + &progress_log_path, + progress_log_template(trimmed, &created_label), + )?; + } + + Ok(MigrationWorkspace { + dir_path, + plan_path, + progress_log_path, + }) +} + +pub(crate) fn slugify_label(label: &str) -> String { + let mut slug = String::new(); + let mut last_was_dash = false; + for ch in label.trim().chars() { + if ch.is_ascii_alphanumeric() { + slug.push(ch.to_ascii_lowercase()); + last_was_dash = false; + } else if matches!(ch, ' ' | '\t' | '-' | '_' | '/' | '.' | ':' | '+' | '&') { + if !slug.is_empty() && !last_was_dash { + slug.push('-'); + last_was_dash = true; + } + } else if !slug.is_empty() && !last_was_dash { + slug.push('-'); + last_was_dash = true; + } + } + slug.trim_matches('-').to_string() +} + +fn next_available_dir(root: &Path, base_name: &str) -> (String, PathBuf) { + let mut counter = 1usize; + loop { + let candidate_name = if counter == 1 { + base_name.to_string() + } else { + format!("{base_name}-{counter}") + }; + let candidate_path = root.join(&candidate_name); + if !candidate_path.exists() { + return (candidate_name, candidate_path); + } + counter += 1; + } +} + +fn initial_plan_template(label: &str, dir_name: &str, created_ts: &str) -> String { + format!( + "# Migration Plan: {label}\n\n_Seeded {created_ts} via `/migrate` (workspace `{dir_name}`)._\n\nUse this document as the canonical playbook. Capture:\n- the current vs. target architecture, data contracts, and release gating.\n- readiness checks before each phase starts.\n- numbered tasks with owners, dependencies, validation, and rollback notes.\n- workstream handoffs plus links to artifacts produced in `progress_log.md`.\n\n## Context\n- Current state:\n- Target state:\n- Non-goals:\n\n## Readiness Gates\n1. _Document prerequisites here._\n\n## Phased Execution Plan\n\n\n## Parallel Workstreams\n| Workstream | Objective | Dependencies | Sync Artifacts |\n| --- | --- | --- | --- |\n\n## Rollout & Rollback\n- Rollout steps:\n- Observability & SLOs:\n- Abort conditions + rollback path:\n\n## Post-migration Hardening\n- Follow-up tasks:\n- Success metrics:\n" + ) +} + +fn progress_log_template(label: &str, created_ts: &str) -> String { + format!( + "# Progress Log: {label}\n\nUse this log so agents can publish async updates other workstreams can learn from. Each row should be timestamped and link to the artifacts or PRs created.\n\n| Timestamp | Owner | Workstream | Update | Next Step |\n| --- | --- | --- | --- | --- |\n| {created_ts} | system | kickoff | Workspace initialized via `/migrate`. | Draft initial migration plan. |\n" + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::tempdir; + + #[test] + fn slugify_handles_symbols() { + assert_eq!( + slugify_label("Payments & Billing 2.0 / EU"), + "payments-billing-2-0-eu" + ); + } + + #[test] + fn slugify_trims_redundant_dashes() { + assert_eq!(slugify_label(" --alpha--beta-- "), "alpha-beta"); + } + + #[test] + fn prepare_workspace_creates_structure() { + let temp = tempdir().unwrap(); + let workspace = prepare_workspace(temp.path(), "Replatform Search").unwrap(); + let dir_name = workspace + .dir_path + .file_name() + .unwrap() + .to_string_lossy() + .into_owned(); + assert!(dir_name.starts_with("migration_replatform-search")); + assert!(workspace.dir_path.exists()); + assert!(workspace.plan_path.exists()); + assert!(workspace.progress_log_path.exists()); + let plan = fs::read_to_string(workspace.plan_path).unwrap(); + assert!(plan.contains("Migration Plan: Replatform Search")); + } + + #[test] + fn prepare_workspace_appends_suffix_when_needed() { + let temp = tempdir().unwrap(); + let first = prepare_workspace(temp.path(), "Observability").unwrap(); + let second = prepare_workspace(temp.path(), "Observability").unwrap(); + let first_name = first + .dir_path + .file_name() + .unwrap() + .to_string_lossy() + .into_owned(); + let second_name = second + .dir_path + .file_name() + .unwrap() + .to_string_lossy() + .into_owned(); + assert_ne!(first_name, second_name); + assert!(second_name.ends_with("-2")); + } + + #[test] + fn prepare_workspace_handles_symbol_only_label() { + let temp = tempdir().unwrap(); + let workspace = prepare_workspace(temp.path(), "***").unwrap(); + let dir_name = workspace + .dir_path + .file_name() + .unwrap() + .to_string_lossy() + .into_owned(); + assert!(dir_name.starts_with("migration_")); + } +} diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 11591f6f9f..26a0344e7e 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -40,7 +40,9 @@ impl SlashCommand { SlashCommand::New => "start a new chat during a conversation", SlashCommand::Init => "create an AGENTS.md file with instructions for Codex", SlashCommand::Compact => "summarize conversation to prevent hitting the context limit", - SlashCommand::Migrate => "plan a large migration by generating numbered tasks", + SlashCommand::Migrate => { + "spin up a migration workspace and produce a collaborative plan" + } SlashCommand::Review => "review my current changes and find issues", SlashCommand::Undo => "ask Codex to undo a turn", SlashCommand::Quit | SlashCommand::Exit => "exit Codex", diff --git a/docs/slash_commands.md b/docs/slash_commands.md index 3f006f333f..a9f45dbf29 100644 --- a/docs/slash_commands.md +++ b/docs/slash_commands.md @@ -17,7 +17,7 @@ Control Codex’s behavior during an interactive session with slash commands. | `/review` | review my current changes and find issues | | `/new` | start a new chat during a conversation | | `/init` | create an AGENTS.md file with instructions for Codex | -| `/migrate` | plan a large migration by generating numbered tasks | +| `/migrate` | create a migration workspace and craft a collaborative plan | | `/compact` | summarize conversation to prevent hitting the context limit | | `/undo` | ask Codex to undo a turn | | `/diff` | show git diff (including untracked files) |