Compare commits

...

2 Commits

Author SHA1 Message Date
rka-oai
9c0c0a3a3a Enhance migrate workflow 2025-11-05 23:08:56 -08:00
rka-oai
ac23107664 Add migrate slash command 2025-11-05 22:20:12 -08:00
8 changed files with 273 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
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.
## 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.
## 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.
## 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;
@@ -1234,6 +1236,9 @@ impl ChatWidget {
const INIT_PROMPT: &str = include_str!("../prompt_for_init_command.md");
self.submit_user_message(INIT_PROMPT.to_string().into());
}
SlashCommand::Migrate => {
self.open_migration_prompt();
}
SlashCommand::Compact => {
self.clear_token_usage();
self.app_event_tx.send(AppEvent::CodexOp(Op::Compact));
@@ -2422,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

@@ -17,6 +17,7 @@ pub enum SlashCommand {
Review,
New,
Init,
Migrate,
Compact,
Undo,
Diff,
@@ -39,6 +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 => {
"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",
@@ -65,6 +69,7 @@ impl SlashCommand {
match self {
SlashCommand::New
| SlashCommand::Init
| SlashCommand::Migrate
| SlashCommand::Compact
| SlashCommand::Undo
| SlashCommand::Model

View File

@@ -17,6 +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` | 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) |