mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
2 Commits
pr-10247
...
codex/add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c0c0a3a3a | ||
|
|
ac23107664 |
23
codex-rs/tui/prompt_for_migrate_command.md
Normal file
23
codex-rs/tui/prompt_for_migrate_command.md
Normal 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.
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
168
codex-rs/tui/src/migration.rs
Normal file
168
codex-rs/tui/src/migration.rs
Normal 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_"));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -17,6 +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` | 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) |
|
||||
|
||||
Reference in New Issue
Block a user