Compare commits

...

1 Commits

Author SHA1 Message Date
rka-oai
c957b4bef1 Enhance migrate command 2025-11-05 23:13:00 -08:00
8 changed files with 264 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
# Migration journal {{MIGRATION_SUMMARY}}
> Workspace: `{{WORKSPACE_NAME}}`
> Created: {{CREATED_AT}}
Use this log for async updates, agent hand-offs, and to publish what was learned during each workstream. Keep entries concise and focused on signals other collaborators need.
## Logging guidance
- Start each entry with a timestamp and author/agent/workstream name.
- Capture what changed, how it was validated, links to diffs/tests, and any open questions.
- Highlight blockers, decisions needed, or knowledge that other agents should adopt.
- Update the plan (`plan.md`) when scope changes; use this journal for progress + lessons.
| Timestamp | Agent / Workstream | Update / Learnings | Blockers & Risks | Next action / owner |
| --------- | ------------------ | ------------------ | ---------------- | ------------------- |
| | | | | |

View File

@@ -0,0 +1,35 @@
# Migration plan {{MIGRATION_SUMMARY}}
> Workspace: `{{WORKSPACE_NAME}}`
> Generated: {{CREATED_AT}}
Use this document as the single source of truth for the migration effort. Keep it updated so any engineer (or agent) can jump in mid-flight.
## 1. Context & stakes
- Current state snapshot
- Target end state and deadline/launch windows
- Guardrails, SLAs, compliance/regulatory constraints
## 2. Incremental plan (numbered)
1. `[Step name]` — Purpose, scope, primary owner/skillset, upstream/downstream dependencies, validation & rollback signals.
2. `…`
Each step must explain:
- Preconditions & artifacts required before starting
- Specific code/data/infrastructure changes
- Telemetry, tests, or dry-runs that prove success
## 3. Parallel workstreams
| Workstream | Objective | Inputs & dependencies | Ownership / skills | Progress & telemetry hooks |
| ---------- | --------- | --------------------- | ------------------ | ------------------------- |
| _Fill in during planning_ | | | | |
## 4. Data + rollout considerations
- Data migration / backfill plan
- Environment readiness, feature flags, or config toggles
- Rollout plan (phases, smoke tests, canaries) and explicit rollback/kill-switch criteria
## 5. Risks, decisions, and follow-ups
- Top risks with mitigation owners
- Open questions / decisions with DRI and due date
- Handoff expectations (reference `journal.md` for ongoing updates)

View File

@@ -0,0 +1,25 @@
You are the migration showrunner orchestrating "{{MIGRATION_SUMMARY}}".
Workspace root: {{WORKSPACE_PATH}}
Plan document to maintain: {{PLAN_PATH}}
Shared journal for progress + learnings: {{JOURNAL_PATH}}
Before writing any code, study the repository layout, dependencies, release process, deployment gates, data stores, and cross-team stakeholders.
Deliverables:
1. Start with a concise **Executive Overview** summarizing the target state, key risks, and the biggest unknowns.
2. Provide an **Incremental plan** as a numbered list (1., 2., 3., …). Each item must include:
- Objective and concrete changes required.
- Dependencies or prerequisites, including approvals and data/state migrations.
- Ownership expectations (skillsets/teams) and automation hooks.
- Validation signals, telemetry, and rollback/kill-switch instructions.
3. Add a **Parallel workstreams** table. Group tasks that can run concurrently, show how learnings are exchanged, and specify when progress should be published to {{JOURNAL_PATH}}.
4. Capture **Coordination & learning loops**: how agents collaborate, what artifacts live in {{PLAN_PATH}} vs. {{JOURNAL_PATH}}, and how to keep successors unblocked.
5. Outline a **Risk / data / rollout** section covering backfills, environment sequencing, feature flags, monitoring, and fallback criteria.
General guidance:
- Call out missing information explicitly and request the files, owners, or metrics needed to proceed.
- Highlight dependencies on other teams, scheduled freezes, or compliance gates.
- Emphasize opportunities for automation, reuse of tooling, and knowledge sharing so multiple agents can contribute safely.
After sharing the plan in chat, mirror the same structure into {{PLAN_PATH}} (using apply_patch or editing tools) so it remains the canonical artifact. Encourage collaborators to keep {{JOURNAL_PATH}} updated with progress, blockers, and learnings.

View File

@@ -450,6 +450,9 @@ impl App {
AppEvent::OpenReviewCustomPrompt => {
self.chat_widget.show_review_custom_prompt();
}
AppEvent::StartMigration { summary } => {
self.chat_widget.start_migration(summary);
}
AppEvent::FullScreenApprovalRequest(request) => match request {
ApprovalRequest::ApplyPatch { cwd, changes, .. } => {
let _ = tui.enter_alt_screen();

View File

@@ -102,6 +102,11 @@ pub(crate) enum AppEvent {
/// Open the custom prompt option from the review popup.
OpenReviewCustomPrompt,
/// Kick off the `/migrate` workflow after the user names the migration.
StartMigration {
summary: String,
},
/// Open the approval popup.
FullScreenApprovalRequest(ApprovalRequest),

View File

@@ -1,5 +1,6 @@
use std::collections::HashMap;
use std::collections::VecDeque;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
@@ -120,10 +121,14 @@ use codex_core::protocol::SandboxPolicy;
use codex_core::protocol_config_types::ReasoningEffort as ReasoningEffortConfig;
use codex_file_search::FileMatch;
use codex_protocol::plan_tool::UpdatePlanArgs;
use pathdiff::diff_paths;
use strum::IntoEnumIterator;
const USER_SHELL_COMMAND_HELP_TITLE: &str = "Prefix a command with ! to run it locally";
const USER_SHELL_COMMAND_HELP_HINT: &str = "Example: !ls";
const MIGRATION_PROMPT_TEMPLATE: &str = include_str!("../prompt_for_migrate_command.md");
const MIGRATION_PLAN_TEMPLATE: &str = include_str!("../migration_plan_template.md");
const MIGRATION_JOURNAL_TEMPLATE: &str = include_str!("../migration_journal_template.md");
// Track information about an in-flight exec command.
struct RunningCommand {
command: Vec<String>,
@@ -1234,6 +1239,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_migrate_prompt();
}
SlashCommand::Compact => {
self.clear_token_usage();
self.app_event_tx.send(AppEvent::CodexOp(Op::Compact));
@@ -2422,6 +2430,132 @@ impl ChatWidget {
self.bottom_pane.show_view(Box::new(view));
}
pub(crate) fn start_migration(&mut self, summary: String) {
let summary = summary.trim();
if summary.is_empty() {
return;
}
match self.create_migration_workspace(summary) {
Ok(workspace) => {
let dir_display = self.relative_display_path(&workspace.dir_path);
let plan_display = self.relative_display_path(&workspace.plan_path);
let journal_display = self.relative_display_path(&workspace.journal_path);
let prompt = self.build_migration_prompt(
summary,
&dir_display,
&plan_display,
&journal_display,
);
let hint = format!("Plan: {plan_display}\nJournal: {journal_display}",);
self.add_info_message(
format!(
"Created migration workspace `{}` at {dir_display}.",
workspace.dir_name
),
Some(hint),
);
self.submit_user_message(prompt.into());
}
Err(err) => {
tracing::error!("failed to prepare migration workspace: {err}");
self.add_error_message(format!("Failed to prepare migration workspace: {err}"));
}
}
}
fn open_migrate_prompt(&mut self) {
let tx = self.app_event_tx.clone();
let view = CustomPromptView::new(
"Describe the migration".to_string(),
"Example: Phase 2 move billing to Postgres".to_string(),
Some(
"We'll create migration_<slug> with plan.md and journal.md once you press Enter."
.to_string(),
),
Box::new(move |prompt: String| {
let trimmed = prompt.trim().to_string();
if trimmed.is_empty() {
return;
}
tx.send(AppEvent::StartMigration { summary: trimmed });
}),
);
self.bottom_pane.show_view(Box::new(view));
}
fn create_migration_workspace(
&self,
summary: &str,
) -> Result<MigrationWorkspace, std::io::Error> {
let slug = sanitize_migration_slug(summary);
let base_name = format!("migration_{slug}");
let (dir_path, dir_name) = self.next_available_migration_dir(&base_name);
fs::create_dir_all(&dir_path)?;
let created_at = Local::now().format("%Y-%m-%d %H:%M %Z").to_string();
let plan_path = dir_path.join("plan.md");
let journal_path = dir_path.join("journal.md");
let plan_contents = fill_template(
MIGRATION_PLAN_TEMPLATE,
&[
("{{MIGRATION_SUMMARY}}", summary),
("{{WORKSPACE_NAME}}", dir_name.as_str()),
("{{CREATED_AT}}", created_at.as_str()),
],
);
let journal_contents = fill_template(
MIGRATION_JOURNAL_TEMPLATE,
&[
("{{MIGRATION_SUMMARY}}", summary),
("{{WORKSPACE_NAME}}", dir_name.as_str()),
("{{CREATED_AT}}", created_at.as_str()),
],
);
fs::write(&plan_path, plan_contents)?;
fs::write(&journal_path, journal_contents)?;
Ok(MigrationWorkspace {
dir_path,
dir_name,
plan_path,
journal_path,
})
}
fn build_migration_prompt(
&self,
summary: &str,
dir_display: &str,
plan_display: &str,
journal_display: &str,
) -> String {
let replacements = [
("{{MIGRATION_SUMMARY}}", summary),
("{{WORKSPACE_PATH}}", dir_display),
("{{PLAN_PATH}}", plan_display),
("{{JOURNAL_PATH}}", journal_display),
];
fill_template(MIGRATION_PROMPT_TEMPLATE, &replacements)
}
fn next_available_migration_dir(&self, base_name: &str) -> (PathBuf, String) {
let mut candidate_name = base_name.to_string();
let mut candidate_path = self.config.cwd.join(&candidate_name);
let mut suffix = 2;
while candidate_path.exists() {
candidate_name = format!("{base_name}_{suffix:02}");
candidate_path = self.config.cwd.join(&candidate_name);
suffix += 1;
}
(candidate_path, candidate_name)
}
fn relative_display_path(&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()
@@ -2580,6 +2714,48 @@ fn extract_first_bold(s: &str) -> Option<String> {
None
}
struct MigrationWorkspace {
dir_path: PathBuf,
dir_name: String,
plan_path: PathBuf,
journal_path: PathBuf,
}
fn fill_template(template: &str, replacements: &[(&str, &str)]) -> String {
let mut filled = template.to_string();
for (needle, value) in replacements {
filled = filled.replace(needle, value);
}
filled
}
fn sanitize_migration_slug(summary: &str) -> String {
let mut slug = String::new();
let mut last_was_dash = true;
for ch in summary.trim().to_lowercase().chars() {
if ch.is_ascii_alphanumeric() {
slug.push(ch);
last_was_dash = false;
} else if !last_was_dash {
slug.push('-');
last_was_dash = true;
}
}
let mut trimmed = slug.trim_matches('-').to_string();
if trimmed.len() > 48 {
trimmed = trimmed
.chars()
.take(48)
.collect::<String>()
.trim_matches('-')
.to_string();
}
if trimmed.is_empty() {
return Local::now().format("plan-%Y%m%d-%H%M%S").to_string();
}
trimmed
}
#[cfg(test)]
pub(crate) fn show_review_commit_picker_with_entries(
chat: &mut ChatWidget,

View File

@@ -17,6 +17,7 @@ pub enum SlashCommand {
Review,
New,
Init,
Migrate,
Compact,
Undo,
Diff,
@@ -39,6 +40,7 @@ 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 planning prompt",
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 +67,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` | spin up a migration workspace and prompt Codex to plan it |
| `/compact` | summarize conversation to prevent hitting the context limit |
| `/undo` | ask Codex to undo a turn |
| `/diff` | show git diff (including untracked files) |