ralph-cli 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. ralph/__init__.py +3 -0
  2. ralph/cli.py +61 -0
  3. ralph/commands/__init__.py +11 -0
  4. ralph/commands/init_cmd.py +358 -0
  5. ralph/commands/loop.py +725 -0
  6. ralph/commands/once.py +239 -0
  7. ralph/commands/prd.py +281 -0
  8. ralph/commands/review.py +330 -0
  9. ralph/commands/sync.py +137 -0
  10. ralph/commands/tasks.py +507 -0
  11. ralph/models/__init__.py +51 -0
  12. ralph/models/config.py +97 -0
  13. ralph/models/finding.py +134 -0
  14. ralph/models/manifest.py +77 -0
  15. ralph/models/review_state.py +97 -0
  16. ralph/models/reviewer.py +165 -0
  17. ralph/models/tasks.py +96 -0
  18. ralph/services/__init__.py +53 -0
  19. ralph/services/claude.py +360 -0
  20. ralph/services/fix_loop.py +294 -0
  21. ralph/services/git.py +177 -0
  22. ralph/services/language.py +96 -0
  23. ralph/services/review_loop.py +461 -0
  24. ralph/services/reviewer_config_writer.py +136 -0
  25. ralph/services/reviewer_detector.py +147 -0
  26. ralph/services/scaffold.py +531 -0
  27. ralph/services/skill_loader.py +185 -0
  28. ralph/services/skills.py +520 -0
  29. ralph/skills/REVIEWER_TEMPLATE.md +228 -0
  30. ralph/skills/SKILL_TEMPLATE.md +192 -0
  31. ralph/skills/__init__.py +5 -0
  32. ralph/skills/ralph/__init__.py +1 -0
  33. ralph/skills/ralph/iteration/SKILL.md +210 -0
  34. ralph/skills/ralph/iteration/__init__.py +1 -0
  35. ralph/skills/ralph/prd/SKILL.md +201 -0
  36. ralph/skills/ralph/prd/__init__.py +1 -0
  37. ralph/skills/ralph/tasks/SKILL.md +250 -0
  38. ralph/skills/ralph/tasks/__init__.py +1 -0
  39. ralph/skills/reviewers/__init__.py +1 -0
  40. ralph/skills/reviewers/code_simplifier/SKILL.md +298 -0
  41. ralph/skills/reviewers/code_simplifier/__init__.py +1 -0
  42. ralph/skills/reviewers/github_actions/SKILL.md +333 -0
  43. ralph/skills/reviewers/github_actions/__init__.py +1 -0
  44. ralph/skills/reviewers/language/__init__.py +1 -0
  45. ralph/skills/reviewers/language/bicep/SKILL.md +394 -0
  46. ralph/skills/reviewers/language/bicep/__init__.py +1 -0
  47. ralph/skills/reviewers/language/python/SKILL.md +363 -0
  48. ralph/skills/reviewers/language/python/__init__.py +1 -0
  49. ralph/skills/reviewers/release/SKILL.md +323 -0
  50. ralph/skills/reviewers/release/__init__.py +1 -0
  51. ralph/skills/reviewers/repo_structure/SKILL.md +295 -0
  52. ralph/skills/reviewers/repo_structure/__init__.py +1 -0
  53. ralph/skills/reviewers/test_quality/SKILL.md +498 -0
  54. ralph/skills/reviewers/test_quality/__init__.py +1 -0
  55. ralph/utils/__init__.py +39 -0
  56. ralph/utils/console.py +126 -0
  57. ralph/utils/files.py +111 -0
  58. ralph/utils/prompt.py +44 -0
  59. ralph_cli-2.2.0.dist-info/METADATA +280 -0
  60. ralph_cli-2.2.0.dist-info/RECORD +63 -0
  61. ralph_cli-2.2.0.dist-info/WHEEL +4 -0
  62. ralph_cli-2.2.0.dist-info/entry_points.txt +2 -0
  63. ralph_cli-2.2.0.dist-info/licenses/LICENSE +21 -0
ralph/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Ralph CLI - Autonomous iteration pattern for Claude Code."""
2
+
3
+ __version__ = "2.2.0"
ralph/cli.py ADDED
@@ -0,0 +1,61 @@
1
+ """Ralph CLI entry point.
2
+
3
+ This module provides the main entry point for the Ralph CLI application,
4
+ a tool for autonomous iteration patterns with Claude Code.
5
+ """
6
+
7
+ import logging
8
+
9
+ import typer
10
+
11
+ from ralph import __version__
12
+ from ralph.commands import init, loop, once, prd, review, sync, tasks
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ app = typer.Typer(
17
+ name="ralph",
18
+ help="Ralph CLI - Autonomous iteration pattern for Claude Code",
19
+ no_args_is_help=True,
20
+ )
21
+
22
+
23
+ def version_callback(value: bool) -> None:
24
+ """Handle the version flag callback.
25
+
26
+ Args:
27
+ value: Whether the version flag was provided.
28
+
29
+ Raises:
30
+ typer.Exit: Always raised after printing version when value is True.
31
+ """
32
+ if value:
33
+ typer.echo(f"ralph {__version__}")
34
+ raise typer.Exit()
35
+
36
+
37
+ @app.callback()
38
+ def main(
39
+ version: bool | None = typer.Option(
40
+ None,
41
+ "--version",
42
+ "-V",
43
+ callback=version_callback,
44
+ is_eager=True,
45
+ help="Show version and exit.",
46
+ ),
47
+ ) -> None:
48
+ """Ralph CLI - Autonomous iteration pattern for Claude Code."""
49
+
50
+
51
+ app.command(name="init", help="Scaffold a project for Ralph workflow")(init)
52
+ app.command(name="prd", help="Create a PRD interactively with Claude")(prd)
53
+ app.command(name="tasks", help="Convert a specification file to TASKS.json")(tasks)
54
+ app.command(name="once", help="Execute a single Ralph iteration")(once)
55
+ app.command(name="loop", help="Run multiple Ralph iterations automatically")(loop)
56
+ app.command(name="sync", help="Sync Ralph skills to Claude Code")(sync)
57
+ app.command(name="review", help="Run the review loop with automatic configuration")(review)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ app()
@@ -0,0 +1,11 @@
1
+ """Ralph CLI commands."""
2
+
3
+ from ralph.commands.init_cmd import init
4
+ from ralph.commands.loop import loop
5
+ from ralph.commands.once import once
6
+ from ralph.commands.prd import prd
7
+ from ralph.commands.review import review
8
+ from ralph.commands.sync import sync
9
+ from ralph.commands.tasks import tasks
10
+
11
+ __all__ = ["init", "prd", "tasks", "once", "loop", "sync", "review"]
@@ -0,0 +1,358 @@
1
+ """Ralph init command - scaffold a project for Ralph workflow.
2
+
3
+ This module implements the 'ralph init' command which creates
4
+ the plans directory structure and configuration files.
5
+ """
6
+
7
+ import logging
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+ import typer
12
+ from rich.prompt import Confirm
13
+
14
+ from ralph.commands.prd import prd as prd_command
15
+ from ralph.services import ClaudeError, ClaudeService, ProjectType, ScaffoldService
16
+ from ralph.utils import console, print_success, print_warning
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def _is_git_repo(project_root: Path) -> bool:
22
+ """Check if the directory is inside a git repository.
23
+
24
+ Args:
25
+ project_root: The directory to check.
26
+
27
+ Returns:
28
+ True if inside a git repo, False otherwise.
29
+ """
30
+ try:
31
+ result = subprocess.run(
32
+ ["git", "rev-parse", "--git-dir"],
33
+ cwd=project_root,
34
+ capture_output=True,
35
+ text=True,
36
+ check=False,
37
+ )
38
+ return result.returncode == 0
39
+ except FileNotFoundError:
40
+ # git not installed
41
+ return False
42
+
43
+
44
+ def _init_git_repo(project_root: Path) -> bool:
45
+ """Initialize a new git repository.
46
+
47
+ Args:
48
+ project_root: The directory to initialize.
49
+
50
+ Returns:
51
+ True if successful, False otherwise.
52
+ """
53
+ try:
54
+ result = subprocess.run(
55
+ ["git", "init"],
56
+ cwd=project_root,
57
+ capture_output=True,
58
+ text=True,
59
+ check=False,
60
+ )
61
+ return result.returncode == 0
62
+ except FileNotFoundError:
63
+ return False
64
+
65
+
66
+ def _create_initial_commit(project_root: Path) -> bool:
67
+ """Create an initial git commit with the scaffolded files.
68
+
69
+ Args:
70
+ project_root: The project root directory.
71
+
72
+ Returns:
73
+ True if successful, False otherwise.
74
+ """
75
+ try:
76
+ add_result = subprocess.run(
77
+ ["git", "add", "."],
78
+ cwd=project_root,
79
+ capture_output=True,
80
+ text=True,
81
+ check=False,
82
+ )
83
+ if add_result.returncode != 0:
84
+ logger.warning(f"git add failed: {add_result.stderr}")
85
+ return False
86
+
87
+ commit_result = subprocess.run(
88
+ ["git", "commit", "-m", "Initial commit: Ralph workflow setup"],
89
+ cwd=project_root,
90
+ capture_output=True,
91
+ text=True,
92
+ check=False,
93
+ )
94
+ # Return code 1 with "nothing to commit" is not an error
95
+ if commit_result.returncode != 0 and "nothing to commit" not in commit_result.stdout:
96
+ logger.warning(f"git commit failed: {commit_result.stderr}")
97
+ return False
98
+
99
+ return True
100
+ except FileNotFoundError:
101
+ return False
102
+
103
+
104
+ def init(
105
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
106
+ skip_claude: bool = typer.Option(
107
+ False, "--skip-claude", help="Skip invoking Claude Code /init"
108
+ ),
109
+ project_name: str | None = typer.Option(
110
+ None, "--name", "-n", help="Project name (defaults to directory name)"
111
+ ),
112
+ ) -> None:
113
+ """Scaffold a project for Ralph workflow.
114
+
115
+ Creates plans/ directory with SPEC.md, TASKS.json, and PROGRESS.txt.
116
+ Also creates CLAUDE.md and AGENTS.md with project-specific defaults.
117
+ """
118
+ project_root = Path.cwd()
119
+
120
+ existing_files = _check_existing_files(project_root)
121
+ if existing_files and not force:
122
+ print_warning("Ralph workflow files already exist:")
123
+ for file in existing_files:
124
+ console.print(f" - {file}")
125
+ console.print()
126
+ console.print("Use [bold]--force[/bold] to overwrite existing files.")
127
+ raise typer.Exit(1)
128
+
129
+ initialized_git = False
130
+ if not _is_git_repo(project_root):
131
+ console.print("[bold]Initializing git repository...[/bold]")
132
+ if _init_git_repo(project_root):
133
+ print_success("Initialized git repository")
134
+ initialized_git = True
135
+ else:
136
+ print_warning("Could not initialize git repository. Git may not be installed.")
137
+ console.print()
138
+
139
+ scaffold = ScaffoldService(project_root=project_root)
140
+ project_type = scaffold.detect_project_type()
141
+
142
+ if project_type != ProjectType.UNKNOWN:
143
+ console.print(f"Detected project type: [bold cyan]{project_type.value}[/bold cyan]")
144
+ else:
145
+ print_warning("Could not detect project type. Using generic template.")
146
+
147
+ # Check for existing PRD content BEFORE scaffolding (scaffolding may overwrite it)
148
+ prd_path = project_root / "plans" / "SPEC.md"
149
+ had_prd_content_before = _has_prd_content(prd_path)
150
+
151
+ console.print()
152
+ console.print("[bold]Creating Ralph workflow files...[/bold]")
153
+
154
+ # Skip CHANGELOG.md creation if it already exists (it's persistent memory)
155
+ changelog_existed = (project_root / "CHANGELOG.md").exists()
156
+ created_files = scaffold.scaffold_all(
157
+ project_name=project_name, skip_changelog=changelog_existed
158
+ )
159
+
160
+ for file_type, path in created_files.items():
161
+ if file_type == "plans_dir":
162
+ continue
163
+ relative_path = path.relative_to(project_root)
164
+ print_success(f"Created {relative_path}")
165
+
166
+ if changelog_existed:
167
+ console.print("[dim] Skipped CHANGELOG.md (already exists)[/dim]")
168
+
169
+ if not had_prd_content_before:
170
+ _handle_missing_prd(prd_path, project_root, skip_claude=skip_claude)
171
+
172
+ if not skip_claude:
173
+ console.print()
174
+ console.print("[bold]Invoking Claude Code /init to enhance project files...[/bold]")
175
+ console.print(
176
+ "[dim]This will analyze your project and update CLAUDE.md and AGENTS.md.[/dim]"
177
+ )
178
+ console.print()
179
+
180
+ try:
181
+ claude = ClaudeService(working_dir=project_root)
182
+ exit_code = claude.run_interactive(
183
+ "/init",
184
+ skip_permissions=True,
185
+ append_system_prompt=ClaudeService.AUTONOMOUS_MODE_PROMPT,
186
+ )
187
+ if exit_code != 0:
188
+ print_warning("Claude Code /init completed with non-zero exit code.")
189
+ except (OSError, ClaudeError) as e:
190
+ print_warning(f"Failed to run Claude Code /init: {e}")
191
+ console.print("[dim]You can run 'claude /init' manually later.[/dim]")
192
+
193
+ if initialized_git:
194
+ console.print()
195
+ console.print("[bold]Creating initial commit...[/bold]")
196
+ if _create_initial_commit(project_root):
197
+ print_success("Created initial commit: 'Initial commit: Ralph workflow setup'")
198
+ else:
199
+ print_warning("Could not create initial commit. You may need to commit manually.")
200
+
201
+ console.print()
202
+ print_success("[bold]Ralph workflow initialized![/bold]")
203
+ console.print()
204
+ console.print("[bold]Next steps:[/bold]")
205
+ console.print(" 1. Edit [cyan]plans/SPEC.md[/cyan] with your feature specification")
206
+ console.print(" Or run [cyan]ralph prd[/cyan] to create one interactively")
207
+ console.print()
208
+ console.print(" 2. Generate tasks from your spec:")
209
+ console.print(" [cyan]ralph tasks plans/SPEC.md[/cyan]")
210
+ console.print()
211
+ console.print(" 3. Start the autonomous iteration loop:")
212
+ console.print(" [cyan]ralph loop[/cyan]")
213
+ console.print()
214
+ console.print("[dim]Tip: Review CLAUDE.md to customize quality checks for your project.[/dim]")
215
+
216
+
217
+ def _check_existing_files(project_root: Path) -> list[str]:
218
+ """Check for existing Ralph workflow files.
219
+
220
+ Args:
221
+ project_root: The project root directory.
222
+
223
+ Returns:
224
+ List of relative paths to existing files.
225
+ """
226
+ files_to_check = [
227
+ "plans/SPEC.md",
228
+ "plans/TASKS.json",
229
+ "plans/PROGRESS.txt",
230
+ "CLAUDE.md",
231
+ "AGENTS.md",
232
+ "CHANGELOG.md",
233
+ ]
234
+
235
+ return [f for f in files_to_check if (project_root / f).exists()]
236
+
237
+
238
+ def _has_prd_content(prd_path: Path) -> bool:
239
+ """Check if the PRD file has meaningful content beyond the template.
240
+
241
+ The scaffolded SPEC.md template contains placeholder brackets like
242
+ `[Describe the feature...]` and `[Goal 1]`. This function checks if
243
+ the user has replaced these placeholders with actual content.
244
+
245
+ Args:
246
+ prd_path: Path to the PRD file (plans/SPEC.md).
247
+
248
+ Returns:
249
+ True if the file has meaningful content, False otherwise.
250
+ """
251
+ if not prd_path.exists():
252
+ return False
253
+
254
+ content = prd_path.read_text().strip()
255
+
256
+ # Empty file has no content
257
+ if not content:
258
+ return False
259
+
260
+ # The scaffold template uses placeholder markers in brackets like:
261
+ # [Describe the feature or project you want to build]
262
+ # [Goal 1], [Goal 2], [Requirement 1], etc.
263
+ # If the file still contains these, it hasn't been filled in
264
+ placeholder_patterns = [
265
+ "[Describe the feature",
266
+ "[Goal 1]",
267
+ "[Requirement 1]",
268
+ "[What this feature will NOT do]",
269
+ "[Describe the high-level architecture]",
270
+ ]
271
+
272
+ # Check if any scaffold placeholder patterns are still present
273
+ has_placeholders = any(pattern in content for pattern in placeholder_patterns)
274
+
275
+ # If it has placeholders, it's still template content
276
+ if has_placeholders:
277
+ return False
278
+
279
+ # Also check for explicit template comment markers
280
+ template_markers = [
281
+ "<!-- Replace this",
282
+ "[Your feature",
283
+ ]
284
+
285
+ if any(marker in content for marker in template_markers):
286
+ return False
287
+
288
+ # Check for actual content: must have at least one section heading followed
289
+ # by actual text (not just another heading or placeholder)
290
+ lines = content.split("\n")
291
+ for i, line in enumerate(lines):
292
+ if line.startswith("## "): # Section heading
293
+ # Check if there's actual content after this heading
294
+ for remaining in lines[i + 1 :]:
295
+ stripped = remaining.strip()
296
+ # Skip empty lines and subheadings
297
+ if not stripped or stripped.startswith("#"):
298
+ continue
299
+ # Found actual content line
300
+ # Make sure it's not just a placeholder bracket
301
+ if stripped.startswith("[") and stripped.endswith("]"):
302
+ continue
303
+ # Found real content
304
+ return True
305
+ break
306
+
307
+ return False
308
+
309
+
310
+ def _handle_missing_prd(prd_path: Path, project_root: Path, *, skip_claude: bool = False) -> None:
311
+ """Handle the case when PRD is missing or empty.
312
+
313
+ Prompts the user to create a PRD using the prd command, or continues
314
+ without one if they decline.
315
+
316
+ Args:
317
+ prd_path: Path to the PRD file (plans/SPEC.md).
318
+ project_root: Path to the project root directory.
319
+ skip_claude: If True, skip the interactive prompt and continue without PRD.
320
+ """
321
+ console.print()
322
+ print_warning("No PRD found at plans/SPEC.md")
323
+ console.print()
324
+ console.print(
325
+ "[dim]A PRD (Product Requirements Document) helps Claude Code understand "
326
+ "your project goals.[/dim]"
327
+ )
328
+ console.print()
329
+
330
+ # In non-interactive mode, skip the prompt and continue without PRD
331
+ if skip_claude:
332
+ console.print(
333
+ "[dim]Skipping PRD creation (--skip-claude mode). "
334
+ "You can create one later with 'ralph prd' or edit plans/SPEC.md directly.[/dim]"
335
+ )
336
+ return
337
+
338
+ if Confirm.ask("Would you like to create a PRD first?", default=True):
339
+ console.print()
340
+ console.print("[bold]Launching PRD creation...[/bold]")
341
+ console.print()
342
+ try:
343
+ # Invoke the prd command to create the specification
344
+ # Use the default output path which is plans/SPEC.md
345
+ # Pass all parameters explicitly to avoid Typer Option defaults not being applied
346
+ prd_command(output=Path("plans/SPEC.md"), verbose=False, input_text=None, file=None)
347
+ except typer.Exit:
348
+ # PRD command completed (either successfully or user cancelled)
349
+ pass
350
+ except (OSError, ClaudeError) as e:
351
+ print_warning(f"PRD creation failed: {e}")
352
+ console.print("[dim]You can create a PRD later with 'ralph prd'.[/dim]")
353
+ else:
354
+ console.print()
355
+ console.print(
356
+ "[dim]Proceeding without PRD. You can create one later with "
357
+ "'ralph prd' or edit plans/SPEC.md directly.[/dim]"
358
+ )