Skip to main content

ralph/cli/
prompt.rs

1//! `ralph prompt ...` command group: Clap types and handler.
2//!
3//! Responsibilities:
4//! - Define clap arguments for prompt preview commands.
5//! - Render worker/scan/task-builder prompts for debugging or inspection.
6//! - List, show, export, and sync prompt templates.
7//!
8//! Not handled here:
9//! - Queue persistence or task status updates.
10//! - Runner execution or model invocation.
11//! - Config file parsing beyond resolved config access.
12//!
13//! Invariants/assumptions:
14//! - Configuration is resolved from the current working directory.
15//! - RepoPrompt selection maps to plan/tool injection consistently.
16//! - Prompt templates are managed via the prompts_internal module.
17
18use anyhow::Result;
19use clap::{Args, Subcommand};
20
21use crate::{
22    agent, commands::prompt as prompt_cmd, commands::task as task_cmd, config, promptflow,
23};
24
25pub fn handle_prompt(args: PromptArgs) -> Result<()> {
26    let resolved = config::resolve_from_cwd()?;
27
28    match args.command {
29        PromptCommand::Worker(p) => {
30            let repoprompt_flags = agent::resolve_repoprompt_flags(p.repo_prompt, &resolved);
31
32            let mode = if p.single {
33                prompt_cmd::WorkerMode::Single
34            } else if let Some(phase) = p.phase {
35                match phase {
36                    promptflow::RunPhase::Phase1 => prompt_cmd::WorkerMode::Phase1,
37                    promptflow::RunPhase::Phase2 => prompt_cmd::WorkerMode::Phase2,
38                    promptflow::RunPhase::Phase3 => prompt_cmd::WorkerMode::Phase3,
39                }
40            } else {
41                // Default behavior: match runtime behavior as closely as possible.
42                // If multi-phase planning is enabled, default to showing Phase 1 prompt (first prompt in the sequence).
43                // Otherwise default to single-phase.
44                if resolved.config.agent.phases.unwrap_or(2) > 1 {
45                    prompt_cmd::WorkerMode::Phase1
46                } else {
47                    prompt_cmd::WorkerMode::Single
48                }
49            };
50
51            let prompt = prompt_cmd::build_worker_prompt(
52                &resolved,
53                prompt_cmd::WorkerPromptOptions {
54                    task_id: p.task_id,
55                    mode,
56                    repoprompt_plan_required: repoprompt_flags.plan_required,
57                    repoprompt_tool_injection: repoprompt_flags.tool_injection,
58                    iterations: p.iterations,
59                    iteration_index: p.iteration_index,
60                    plan_file: p.plan_file,
61                    plan_text: p.plan_text,
62                    explain: p.explain,
63                },
64            )?;
65            print!("{prompt}");
66        }
67        PromptCommand::Scan(p) => {
68            let rp_required = agent::resolve_rp_required(p.repo_prompt, &resolved);
69            let prompt = prompt_cmd::build_scan_prompt(
70                &resolved,
71                prompt_cmd::ScanPromptOptions {
72                    focus: p.focus,
73                    mode: p.mode,
74                    repoprompt_tool_injection: rp_required,
75                    explain: p.explain,
76                },
77            )?;
78            print!("{prompt}");
79        }
80        PromptCommand::TaskBuilder(p) => {
81            let rp_required = agent::resolve_rp_required(p.repo_prompt, &resolved);
82
83            // For convenience, allow stdin usage like `task` does.
84            let request = if let Some(r) = p.request {
85                r
86            } else {
87                // Re-use existing behavior to keep semantics consistent.
88                task_cmd::read_request_from_args_or_stdin(&[])? // will read stdin if piped
89            };
90
91            let prompt = prompt_cmd::build_task_builder_prompt(
92                &resolved,
93                prompt_cmd::TaskBuilderPromptOptions {
94                    request,
95                    hint_tags: p.tags,
96                    hint_scope: p.scope,
97                    repoprompt_tool_injection: rp_required,
98                    explain: p.explain,
99                },
100            )?;
101            print!("{prompt}");
102        }
103        PromptCommand::List => {
104            prompt_cmd::list_prompts(&resolved.repo_root)?;
105        }
106        PromptCommand::Show(p) => {
107            prompt_cmd::show_prompt(&resolved.repo_root, &p.name, p.raw)?;
108        }
109        PromptCommand::Export(p) => {
110            prompt_cmd::export_prompts(&resolved.repo_root, p.name.as_deref(), p.force)?;
111        }
112        PromptCommand::Sync(p) => {
113            prompt_cmd::sync_prompts(&resolved.repo_root, p.dry_run, p.force)?;
114        }
115        PromptCommand::Diff(p) => {
116            prompt_cmd::diff_prompt(&resolved.repo_root, &p.name)?;
117        }
118    }
119
120    Ok(())
121}
122
123fn parse_phase(s: &str) -> anyhow::Result<promptflow::RunPhase> {
124    match s {
125        "1" => Ok(promptflow::RunPhase::Phase1),
126        "2" => Ok(promptflow::RunPhase::Phase2),
127        "3" => Ok(promptflow::RunPhase::Phase3),
128        _ => anyhow::bail!("invalid phase '{s}', expected 1, 2, or 3"),
129    }
130}
131
132#[derive(Args)]
133#[command(
134    about = "Manage and inspect prompt templates",
135    after_long_help = "Commands to view, export, and sync prompt templates.\n\nPreview compiled prompts (what the agent sees):\n  ralph prompt worker --phase 1 --repo-prompt plan\n  ralph prompt worker --single\n  ralph prompt scan --focus \"risk audit\" --repo-prompt off\n  ralph prompt task-builder --request \"Add tests\"\n\nList and view raw templates:\n  ralph prompt list\n  ralph prompt show worker --raw\n  ralph prompt diff worker\n\nExport and sync templates:\n  ralph prompt export --all\n  ralph prompt export worker\n  ralph prompt sync --dry-run\n  ralph prompt sync --force\n"
136)]
137pub struct PromptArgs {
138    #[command(subcommand)]
139    pub command: PromptCommand,
140}
141
142#[derive(Subcommand)]
143pub enum PromptCommand {
144    /// Render the worker prompt (single-phase or phase 1/2/3).
145    Worker(PromptWorkerArgs),
146    /// Render the scan prompt.
147    Scan(PromptScanArgs),
148    /// Render the task-builder prompt.
149    TaskBuilder(PromptTaskBuilderArgs),
150    /// List all available prompt templates.
151    List,
152    /// Show a specific prompt template (raw embedded or effective).
153    Show(PromptShowArgs),
154    /// Export embedded prompts to .ralph/prompts/ for customization.
155    Export(PromptExportArgs),
156    /// Sync exported prompts with embedded defaults.
157    Sync(PromptSyncArgs),
158    /// Show diff between user override and embedded default.
159    Diff(PromptDiffArgs),
160}
161
162#[derive(Args)]
163pub struct PromptWorkerArgs {
164    /// Force worker single-phase prompt (plan+implement in one prompt) even if two-pass is enabled.
165    #[arg(long, conflicts_with = "phase")]
166    pub single: bool,
167
168    /// Force a specific worker phase (1=Plan, 2=Implement).
169    #[arg(long, value_parser = parse_phase)]
170    pub phase: Option<promptflow::RunPhase>,
171
172    /// Task id to use for status-update instructions (defaults to first todo task).
173    #[arg(long)]
174    pub task_id: Option<String>,
175
176    /// For phase 2: path to a plan file to embed.
177    #[arg(long)]
178    pub plan_file: Option<std::path::PathBuf>,
179
180    /// For phase 2: inline plan text (takes precedence over --plan-file and cache).
181    #[arg(long)]
182    pub plan_text: Option<String>,
183
184    /// Simulate total iteration count for prompt preview.
185    #[arg(long, default_value_t = 1)]
186    pub iterations: u8,
187
188    /// Simulate which iteration index to preview (1-based).
189    #[arg(long, default_value_t = 1)]
190    pub iteration_index: u8,
191
192    /// RepoPrompt mode (tools, plan, off). Alias: -rp.
193    #[arg(long = "repo-prompt", value_enum, value_name = "MODE")]
194    pub repo_prompt: Option<agent::RepoPromptMode>,
195
196    /// Print a header explaining what was selected (mode, sources, flags).
197    #[arg(long)]
198    pub explain: bool,
199}
200
201#[derive(Args)]
202pub struct PromptScanArgs {
203    /// Optional scan focus prompt.
204    #[arg(long, default_value = "")]
205    pub focus: String,
206
207    /// Scan mode: maintenance (default) for code hygiene and bug finding,
208    /// innovation for feature discovery and enhancement opportunities.
209    #[arg(short = 'm', long, value_enum, default_value_t = super::scan::ScanMode::Maintenance)]
210    pub mode: super::scan::ScanMode,
211
212    /// RepoPrompt mode (tools, plan, off). Alias: -rp.
213    #[arg(long = "repo-prompt", value_enum, value_name = "MODE")]
214    pub repo_prompt: Option<agent::RepoPromptMode>,
215
216    /// Print a header explaining what was selected (sources, flags).
217    #[arg(long)]
218    pub explain: bool,
219}
220
221#[derive(Args)]
222pub struct PromptTaskBuilderArgs {
223    /// Freeform request text; if omitted, reads from stdin.
224    #[arg(long)]
225    pub request: Option<String>,
226
227    /// Optional hint tags (passed to the task builder prompt).
228    #[arg(long, default_value = "")]
229    pub tags: String,
230
231    /// Optional hint scope (passed to the task builder prompt).
232    #[arg(long, default_value = "")]
233    pub scope: String,
234
235    /// RepoPrompt mode (tools, plan, off). Alias: -rp.
236    #[arg(long = "repo-prompt", value_enum, value_name = "MODE")]
237    pub repo_prompt: Option<agent::RepoPromptMode>,
238
239    /// Print a header explaining what was selected (sources, flags).
240    #[arg(long)]
241    pub explain: bool,
242}
243
244#[derive(Args)]
245pub struct PromptShowArgs {
246    /// Template name (e.g., worker, worker_phase1, scan).
247    pub name: String,
248
249    /// Show raw embedded content instead of effective (with override).
250    #[arg(long)]
251    pub raw: bool,
252}
253
254#[derive(Args)]
255pub struct PromptExportArgs {
256    /// Template name to export (e.g., worker). If omitted and --all not set, errors.
257    pub name: Option<String>,
258
259    /// Export all templates.
260    #[arg(long)]
261    pub all: bool,
262
263    /// Overwrite existing files.
264    #[arg(long)]
265    pub force: bool,
266}
267
268#[derive(Args)]
269pub struct PromptSyncArgs {
270    /// Preview changes without applying.
271    #[arg(long)]
272    pub dry_run: bool,
273
274    /// Overwrite user modifications without prompting.
275    #[arg(long)]
276    pub force: bool,
277}
278
279#[derive(Args)]
280pub struct PromptDiffArgs {
281    /// Template name to diff (e.g., worker).
282    pub name: String,
283}