feat: add PR description generation (argus describe)#3
Conversation
Add 'argus describe' subcommand that generates PR titles, descriptions, and labels from diffs using LLM analysis. Features: - Conventional commit-style title generation (max 72 chars) - Structured description with summary, changes, and considerations - Label suggestions from common categories - Supports stdin, --file, and --pr input sources - Repository context via --repo for better descriptions - Progress spinner with elapsed time - All output formats: text, json, markdown Implementation: - PrDescription struct (Serialize + Deserialize) - build_describe_system_prompt() - LLM system instructions - build_describe_prompt() - user prompt with diff + context - parse_describe_response() - JSON response parser with fence stripping - Full CLI integration with error handling and spinners Also adds: - SPRINT-CONTROL and SPRINT-LOG.md for sprint tracking - SPRINT-TEMPLATE.md for sprint workflow documentation - Fixes clippy warnings (map_err -> inspect_err) Tests: 372 passing (10 new tests for describe feature) Entire-Checkpoint: b934bccc5ebd
📝 WalkthroughWalkthroughThis pull request introduces a PR description generation feature via a new Describe subcommand that leverages LLM capabilities to create titles, descriptions, and labels from diffs. It also adds the indicatif dependency, includes sprint documentation and control files, and scaffolds incremental review flags in the Review command. Changes
Sequence Diagram(s)sequenceDiagram
participant CLI as CLI / main.rs
participant Validator as API Validator
participant RepoMap as Repo Mapper
participant LLM as LLM Client
participant Parser as Response Parser
participant Formatter as Output Formatter
CLI->>Validator: Validate LLM API key
Validator-->>CLI: Key validated or error
CLI->>RepoMap: Extract optional repo structure
RepoMap-->>CLI: Repo map context
CLI->>LLM: build_describe_system_prompt() + build_describe_prompt(diff, repo_map, history)
LLM->>LLM: Generate description via LLM API
LLM-->>CLI: LLM response (JSON)
CLI->>Parser: parse_describe_response(response)
Parser-->>CLI: PrDescription {title, description, labels}
CLI->>Formatter: Format by OutputFormat (Json/Markdown/Text)
Formatter-->>CLI: Formatted output
CLI->>CLI: Display result + labels (if applicable)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@SPRINT-CONTROL`:
- Around line 1-4: The SPRINT-CONTROL file is a local automation state that
shouldn't be committed; remove the file from the repository and add an ignore
rule for its name (referencing SPRINT-CONTROL and the PAUSED variable) to
.gitignore so future commits don't include it, and if it was already committed
remove it from the git index (unstage/delete from repo while keeping any local
copy as needed) and commit the .gitignore update.
In `@SPRINT-TEMPLATE.md`:
- Around line 1-44: Remove the personal/private workflow content from
SPRINT-TEMPLATE.md: delete or replace the explicit local paths
(~/.openclaw/workspace/research/..., ~/.claude/settings.json), proxy config
("Proxy on port 8080") and any developer-specific instructions with generic,
non-sensitive placeholders or move the detailed notes into a personal notes file
outside the repo; add guidance in the template to use project-level docs for
shared settings and add the sensitive file paths to .gitignore (or remove the
file from the commit history if already pushed) so that strings like
"~/.openclaw/workspace/research", "~/.claude/settings.json", and "port 8080" are
no longer present in the public repo.
In `@src/main.rs`:
- Around line 192-204: The incremental and base_sha clap flags are parsed but
currently ignored; either hide them until implemented by adding #[arg(hide =
true)] to the incremental and base_sha struct fields, or add a runtime check in
the Review match arm that detects if incremental || base_sha.is_some() and
immediately returns a clear error (e.g., using miette::bail! with a "not yet
implemented" message and link to the issue) before any review logic runs; update
the Review match arm and the incremental/base_sha declarations accordingly so
users aren't misled.
🧹 Nitpick comments (3)
SPRINT-LOG.md (1)
1-25: Consider consolidating sprint process artifacts.Both
SPRINT-LOG.mdandSPRINT-CONTROLare internal sprint-chain automation artifacts. If you want to track shipped features, a conventionalCHANGELOG.mdwould be more appropriate and recognizable to contributors. Otherwise, consider.gitignore-ing these files to keep the repo focused on project content.crates/argus-review/src/prompt.rs (1)
438-461: Optional: extract shared prompt-building helpers to reduce duplication.The repo-map and history-context sections in
build_describe_prompt(lines 445–455) are near-identical tobuild_review_prompt(lines 118–134). Consider extracting a small helper likeappend_context_sections(prompt, repo_map, history_context)to DRY this up, especially if more prompt builders are added in the future.src/main.rs (1)
1281-1402: Describe implementation looks solid overall. A few observations:
Diff input, API key checking, and empty-diff validation (lines 1290–1317) are near-identical to the Review command. Consider extracting shared helpers (e.g.,
resolve_diff_input,require_llm_api_key) to reduce duplication — not urgent but worth tracking.Lost LLM response on parse failure (line 1369–1374): When
parse_describe_responsefails, the raw LLM output is discarded. In verbose mode, logging the raw response to stderr would help debugging malformed LLM output.Log raw response on parse failure in verbose mode
let desc = - argus_review::prompt::parse_describe_response(&response).inspect_err(|_e| { + argus_review::prompt::parse_describe_response(&response).inspect_err(|e| { if let Some(pb) = &spinner { pb.finish_with_message("Failed to parse response"); } + if cli.verbose { + eprintln!("Raw LLM response:\n{response}"); + } })?;
| # Sprint Control | ||
| # Delete this file or set PAUSED=true to stop the sprint chain. | ||
|
|
||
| PAUSED=false |
There was a problem hiding this comment.
Sprint control file should not be committed to the repository.
This is a local automation state file for your personal sprint workflow. It doesn't serve the project's users or contributors and will cause confusion or merge conflicts. Add it to .gitignore instead.
🤖 Prompt for AI Agents
In `@SPRINT-CONTROL` around lines 1 - 4, The SPRINT-CONTROL file is a local
automation state that shouldn't be committed; remove the file from the
repository and add an ignore rule for its name (referencing SPRINT-CONTROL and
the PAUSED variable) to .gitignore so future commits don't include it, and if it
was already committed remove it from the git index (unstage/delete from repo
while keeping any local copy as needed) and commit the .gitignore update.
| # Argus Sprint Template | ||
| # This file is the canonical sprint prompt. Each new sprint session reads this. | ||
| # Update this file as features ship and new ones are identified. | ||
|
|
||
| ## What Is Argus | ||
| AI code review CLI tool in Rust. One binary, 8 crates. Repo: ~/argus, GitHub: Meru143/argus. | ||
|
|
||
| ## Research | ||
| Always read before implementing: | ||
| - ~/.openclaw/workspace/research/argus/competitive-deep-dive.md | ||
| - ~/.openclaw/workspace/research/argus/claude-code-best-practices-report.md (MUST READ before CC) | ||
| - ~/.openclaw/workspace/research/argus/PROJECT-PLAN-REVIEW.md | ||
| - ~/argus/SPRINT-LOG.md (what's already shipped) | ||
|
|
||
| ## Feature Backlog (update as features ship) | ||
| 1. PR description generation — ~80% done in dirty tree | ||
| 2. Incremental review — only review NEW changes after force-push | ||
| 3. Hotspot-aware review prioritization — USE gitpulse data | ||
| 4. Learning from feedback — thumbs up/down → persistent rules | ||
| 5. More tree-sitter languages — PHP, Kotlin, Swift | ||
| 6. [When backlog empty: research competitors, identify new features, never stop] | ||
|
|
||
| ## Rules | ||
| - Individual commits per feature (NO squash merge) | ||
| - cargo test + clippy + fmt before committing | ||
| - gh run list after each push | ||
| - Update ~/argus/SPRINT-LOG.md after each feature | ||
| - Disk: run cargo clean if needed (~13GB free) | ||
| - When backlog empty: research new features autonomously | ||
|
|
||
| ## CC Orchestration | ||
| - READ the CC best practices report BEFORE starting CC | ||
| - tmux skill for CC control | ||
| - One task per CC session, /clear between | ||
| - Proxy on port 8080, settings at ~/.claude/settings.json | ||
| - Rate limits: wait and retry | ||
| - Stuck/degraded: kill and restart | ||
| - Write state to files before /clear | ||
|
|
||
| ## Sprint Chain | ||
| When done: | ||
| 1. Check ~/argus/SPRINT-CONTROL — if PAUSED=true or missing, stop | ||
| 2. If not paused, schedule wake event in 5 min: "SPRINT_CHAIN: Start next argus sprint" | ||
| 3. Report what shipped |
There was a problem hiding this comment.
Remove personal/private workflow document from the repository.
This file contains local filesystem paths (~/.openclaw/workspace/research/..., ~/.claude/settings.json), proxy configuration details (port 8080), and developer-specific workflow instructions. Committing this to a public repository:
- Leaks private directory structures and tooling setup
- Is not useful to other contributors
- Could be a minor security concern (exposing internal infrastructure details)
This should be in a personal notes directory or excluded via .gitignore.
🤖 Prompt for AI Agents
In `@SPRINT-TEMPLATE.md` around lines 1 - 44, Remove the personal/private workflow
content from SPRINT-TEMPLATE.md: delete or replace the explicit local paths
(~/.openclaw/workspace/research/..., ~/.claude/settings.json), proxy config
("Proxy on port 8080") and any developer-specific instructions with generic,
non-sensitive placeholders or move the detailed notes into a personal notes file
outside the repo; add guidance in the template to use project-level docs for
shared settings and add the sensitive file paths to .gitignore (or remove the
file from the commit history if already pushed) so that strings like
"~/.openclaw/workspace/research", "~/.claude/settings.json", and "port 8080" are
no longer present in the public repo.
| /// Incremental review: only review changes since the last review | ||
| #[arg( | ||
| long, | ||
| long_help = "Enable incremental review mode.\n\n\ | ||
| Only review hunks that are NEW or CHANGED since the last review.\n\ | ||
| Compares the current diff against a saved review state in .argus/review-state.json.\n\ | ||
| On first run (no saved state), reviews everything and saves state.\n\ | ||
| Use --base-sha to explicitly set the comparison point." | ||
| )] | ||
| incremental: bool, | ||
| /// Base commit SHA for incremental review (overrides saved state) | ||
| #[arg(long)] | ||
| base_sha: Option<String>, |
There was a problem hiding this comment.
incremental and base_sha flags are accepted but silently ignored — this will confuse users.
These flags are parsed by clap and documented with detailed long_help, but they're bound to _ at lines 1080–1081. A user running argus review --incremental will get no indication that the flag had no effect. Either gate behind a #[arg(hide = true)] attribute to hide them until implemented, or bail with a "not yet implemented" message at runtime.
Option A: Hide the flags until implemented
- /// Incremental review: only review changes since the last review
- #[arg(
- long,
- long_help = "Enable incremental review mode.\n\n\
- Only review hunks that are NEW or CHANGED since the last review.\n\
- Compares the current diff against a saved review state in .argus/review-state.json.\n\
- On first run (no saved state), reviews everything and saves state.\n\
- Use --base-sha to explicitly set the comparison point."
- )]
- incremental: bool,
- /// Base commit SHA for incremental review (overrides saved state)
- #[arg(long)]
- base_sha: Option<String>,
+ /// Incremental review: only review changes since the last review
+ #[arg(long, hide = true)]
+ incremental: bool,
+ /// Base commit SHA for incremental review (overrides saved state)
+ #[arg(long, hide = true)]
+ base_sha: Option<String>,Option B: Bail at runtime
Add this inside the Review match arm (before the review logic):
if incremental || base_sha.is_some() {
miette::bail!("Incremental review is not yet implemented. Follow progress at https://github.com/Meru143/argus/issues/...");
}🤖 Prompt for AI Agents
In `@src/main.rs` around lines 192 - 204, The incremental and base_sha clap flags
are parsed but currently ignored; either hide them until implemented by adding
#[arg(hide = true)] to the incremental and base_sha struct fields, or add a
runtime check in the Review match arm that detects if incremental ||
base_sha.is_some() and immediately returns a clear error (e.g., using
miette::bail! with a "not yet implemented" message and link to the issue) before
any review logic runs; update the Review match arm and the incremental/base_sha
declarations accordingly so users aren't misled.
There was a problem hiding this comment.
Pull request overview
This PR adds a new argus describe subcommand that generates PR titles, descriptions, and labels from code diffs using LLM analysis. The feature provides a complementary capability to the existing argus review command, focusing on automated PR documentation rather than code review.
Changes:
- New
describeCLI subcommand with support for stdin, file, and PR inputs - Prompt engineering infrastructure for PR description generation with JSON response parsing
- Sprint orchestration files (SPRINT-CONTROL, SPRINT-LOG.md, SPRINT-TEMPLATE.md) for autonomous development workflow
- Progress spinner with elapsed time display during LLM calls
- 10 new unit tests covering prompt building and response parsing
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main.rs | Added Describe command enum variant, CLI argument parsing, and command handler with diff fetching, LLM interaction, and multi-format output (JSON/Markdown/Text) |
| crates/argus-review/src/prompt.rs | Added build_describe_system_prompt(), build_describe_prompt(), PrDescription struct, and parse_describe_response() with comprehensive unit tests |
| Cargo.toml | Added indicatif dependency to main binary for progress indicators |
| Cargo.lock | Updated lock file with indicatif dependency |
| SPRINT-TEMPLATE.md | Sprint orchestration template documenting development workflow, feature backlog, and automation rules |
| SPRINT-LOG.md | Sprint history log documenting previously shipped features |
| SPRINT-CONTROL | Sprint control file with pause/resume flag for autonomous development chain |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Generate repo map if a repo path is provided | ||
| let repo_map = if let Some(root) = repo { | ||
| let diffs = argus_difflens::parser::parse_unified_diff(&diff_input)?; | ||
| let focus_files: Vec<std::path::PathBuf> = | ||
| diffs.iter().map(|d| d.new_path.clone()).collect(); | ||
| match argus_repomap::generate_map(root, 1024, &focus_files, OutputFormat::Text) { | ||
| Ok(map) if !map.is_empty() => Some(map), | ||
| _ => None, | ||
| } | ||
| } else { | ||
| None | ||
| }; |
There was a problem hiding this comment.
Code duplication: The repo map generation logic (lines 1319-1330) is very similar to the review command's implementation in pipeline.rs (lines 209-218). While some duplication is acceptable here due to different contexts (CLI vs library), consider whether this logic should be extracted into a shared helper function in the argus-repomap crate.
| let is_tty = std::io::stderr().is_terminal(); | ||
| let spinner = if is_tty { | ||
| let pb = indicatif::ProgressBar::new_spinner(); | ||
| pb.set_style( | ||
| indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg} ({elapsed})") | ||
| .unwrap(), | ||
| ); | ||
| pb.set_message("Generating PR description..."); | ||
| pb.enable_steady_tick(std::time::Duration::from_millis(120)); | ||
| Some(pb) | ||
| } else { | ||
| None | ||
| }; |
There was a problem hiding this comment.
The spinner setup logic (lines 1334-1346) is duplicated from the review command's pattern in pipeline.rs. The codebase already has a make_spinner helper function in pipeline.rs (lines 559-568). Consider either reusing that helper or extracting the spinner logic into a shared utility function to reduce duplication.
| # Sprint Control | ||
| # Delete this file or set PAUSED=true to stop the sprint chain. | ||
|
|
||
| PAUSED=false |
There was a problem hiding this comment.
Sprint infrastructure files (SPRINT-CONTROL, SPRINT-LOG.md, SPRINT-TEMPLATE.md) appear to be internal automation/development files and should likely be added to .gitignore instead of being committed to the repository. These files seem to be used for autonomous sprint orchestration and are not part of the public-facing project functionality.
| PAUSED=false | |
| PAUSED=true |
| let user = | ||
| argus_review::prompt::build_describe_prompt(&diff_input, repo_map.as_deref(), None); |
There was a problem hiding this comment.
The describe command's prompt builder accepts a history_context parameter (line 1350), but this context is never generated or passed in. Consider adding git history context generation (similar to the review command in pipeline.rs:233-237) to provide richer context for PR description generation, or remove the unused parameter from the prompt builder if it's not needed.
| ref repo, | ||
| }) => { | ||
| if cli.format == OutputFormat::Sarif { | ||
| miette::bail!("SARIF output is not supported for the describe subcommand."); |
There was a problem hiding this comment.
Inconsistent error message format. Other commands use "SARIF output is only supported for the review subcommand" (lines 709, 739, 849), but describe uses "SARIF output is not supported for the describe subcommand". Consider using the positive form for consistency: "SARIF output is only supported for the review subcommand."
| miette::bail!("SARIF output is not supported for the describe subcommand."); | |
| miette::bail!("SARIF output is only supported for the review subcommand."); |
| let diff_input = if let Some(pr_ref) = pr { | ||
| let (owner, repo, pr_number) = argus_review::github::parse_pr_reference(pr_ref)?; | ||
| let github = argus_review::github::GitHubClient::new(None)?; | ||
| github.get_pr_diff(&owner, &repo, pr_number).await? | ||
| } else { | ||
| read_diff_input(file)? | ||
| }; |
There was a problem hiding this comment.
Code duplication: The diff input fetching logic (lines 1290-1296) is identical to the review command's implementation (lines 1091-1097). Consider extracting this into a helper function to reduce duplication and improve maintainability. For example, a function like async fn fetch_diff_input(pr: &Option<String>, file: &Option<PathBuf>) -> Result<String> could be shared between both commands.
| let diff_input = if let Some(pr_ref) = pr { | |
| let (owner, repo, pr_number) = argus_review::github::parse_pr_reference(pr_ref)?; | |
| let github = argus_review::github::GitHubClient::new(None)?; | |
| github.get_pr_diff(&owner, &repo, pr_number).await? | |
| } else { | |
| read_diff_input(file)? | |
| }; | |
| let diff_input = fetch_diff_input(pr, file).await?; |
| if diff_input.trim().is_empty() && pr.is_none() { | ||
| miette::bail!(miette::miette!( | ||
| help = "Pipe a diff to argus, e.g.: git diff main | argus describe\n Or use --file <path> or --pr owner/repo#123", | ||
| "Empty diff input" | ||
| )); | ||
| } |
There was a problem hiding this comment.
Code duplication: The empty diff input check and error message (lines 1298-1303) is nearly identical to the review command's implementation (lines 1100-1107). The only difference is "git diff main | argus describe" vs "git diff | argus review --repo .". Consider extracting this validation into a helper function that accepts the command name as a parameter to reduce duplication.
| // Hint: missing API key | ||
| let llm_env_var = match config.llm.provider.as_str() { | ||
| "anthropic" => "ANTHROPIC_API_KEY", | ||
| "gemini" => "GEMINI_API_KEY", | ||
| _ => "OPENAI_API_KEY", | ||
| }; | ||
| if config.llm.api_key.is_none() && std::env::var(llm_env_var).is_err() { | ||
| miette::bail!(miette::miette!( | ||
| help = "Set {llm_env_var} or add api_key in your .argus.toml under [llm]", | ||
| "No API key configured for LLM provider '{}'", | ||
| config.llm.provider | ||
| )); | ||
| } |
There was a problem hiding this comment.
Code duplication: The LLM API key validation (lines 1305-1317) is identical to the review command's implementation (lines 1133-1145). Consider extracting this into a helper function to validate API key configuration, reducing duplication across commands.
feat: add PR description generation (argus describe)
Summary
Add
argus describesubcommand that generates PR titles, descriptions, and labels from diffs using LLM analysis.Changes
argus describe— generates PR title, description, and labels from code diffsbuild_describe_system_prompt(),build_describe_prompt(),parse_describe_response()map_err→inspect_errfor side-effect-only error handlersTesting
cargo clippy --workspace -- -D warningscleancargo fmt --checkcleanUsage
Summary by CodeRabbit
New Features
Describecommand to automatically generate PR titles, descriptions, and labels from diffs or pull requestsChores