Enhance migrate workflow

This commit is contained in:
rka-oai
2025-11-05 23:08:56 -08:00
parent ac23107664
commit 9c0c0a3a3a
8 changed files with 266 additions and 13 deletions

View File

@@ -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.

View File

@@ -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();
}

View File

@@ -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)]

View File

@@ -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_<name> 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()

View File

@@ -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;

View File

@@ -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<MigrationWorkspace> {
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<!-- Expand each phase with entry criteria, tasks, validation, and exit signals. -->\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_"));
}
}

View File

@@ -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",

View File

@@ -17,7 +17,7 @@ Control Codexs 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) |