ralph-code 0.5.0__tar.gz → 0.6.2__tar.gz

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 (43) hide show
  1. {ralph_code-0.5.0/ralph_code.egg-info → ralph_code-0.6.2}/PKG-INFO +13 -4
  2. {ralph_code-0.5.0 → ralph_code-0.6.2}/README.md +12 -3
  3. {ralph_code-0.5.0 → ralph_code-0.6.2}/pyproject.toml +10 -1
  4. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/__init__.py +1 -1
  5. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/__main__.py +8 -0
  6. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/app.py +113 -11
  7. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/config.py +46 -2
  8. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/git_manager.py +8 -2
  9. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/harness.py +8 -2
  10. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/harness_runner.py +194 -43
  11. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/prd_manager.py +19 -3
  12. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/schemas/ralph_tasks_schema.json +2 -2
  13. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/workflow.py +84 -20
  14. {ralph_code-0.5.0 → ralph_code-0.6.2/ralph_code.egg-info}/PKG-INFO +13 -4
  15. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph_code.egg-info/SOURCES.txt +11 -1
  16. {ralph_code-0.5.0 → ralph_code-0.6.2}/setup.py +1 -1
  17. ralph_code-0.6.2/tests/test_app.py +796 -0
  18. ralph_code-0.6.2/tests/test_app_integration.py +825 -0
  19. ralph_code-0.6.2/tests/test_colors.py +155 -0
  20. ralph_code-0.6.2/tests/test_config.py +367 -0
  21. ralph_code-0.6.2/tests/test_git_manager.py +415 -0
  22. {ralph_code-0.5.0 → ralph_code-0.6.2}/tests/test_harness.py +25 -6
  23. {ralph_code-0.5.0 → ralph_code-0.6.2}/tests/test_harness_runner.py +105 -9
  24. ralph_code-0.6.2/tests/test_prd_manager.py +435 -0
  25. ralph_code-0.6.2/tests/test_storage.py +210 -0
  26. ralph_code-0.6.2/tests/test_tasks.py +380 -0
  27. ralph_code-0.6.2/tests/test_user_stories.py +631 -0
  28. ralph_code-0.6.2/tests/test_workflow.py +470 -0
  29. {ralph_code-0.5.0 → ralph_code-0.6.2}/LICENSE +0 -0
  30. {ralph_code-0.5.0 → ralph_code-0.6.2}/MANIFEST.in +0 -0
  31. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/claude_runner.py +0 -0
  32. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/colors.py +0 -0
  33. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/schemas/task_schema.json +0 -0
  34. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/spinner.py +0 -0
  35. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/storage.py +0 -0
  36. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/tasks.py +0 -0
  37. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph/user_stories.py +0 -0
  38. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph_code.egg-info/dependency_links.txt +0 -0
  39. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph_code.egg-info/entry_points.txt +0 -0
  40. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph_code.egg-info/requires.txt +0 -0
  41. {ralph_code-0.5.0 → ralph_code-0.6.2}/ralph_code.egg-info/top_level.txt +0 -0
  42. {ralph_code-0.5.0 → ralph_code-0.6.2}/setup.cfg +0 -0
  43. {ralph_code-0.5.0 → ralph_code-0.6.2}/tests/test_spinner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ralph-code
3
- Version: 0.5.0
3
+ Version: 0.6.2
4
4
  Summary: Automated task implementation with Claude Code and Codex
5
5
  Author: Ralph Coding
6
6
  License: MIT
@@ -37,7 +37,7 @@ Dynamic: requires-python
37
37
 
38
38
  # ralph-code
39
39
 
40
- Automated task implementation with Claude Code and Codex for "Ralph Coding". What is [Ralph Coding](https://ghuntley.com/ralph/)? It's a method of coding where context rot is avoided by controlling the retention of information. This method involves re-invoking claude or codex for each task, and passing information about the requirements, acceptance testing, and any progress that's made (or roadblocks/challenges faced) through files, rather than retaining all prompts + thinking + response tokens. It tends to result in more requests, some duplicated token work, but fairly consistent performance, and best of all it can largely be done unattended. Recommend Claude Max account or codex equivalent, but be aware that GPT-5 - GPT5.2's slow reasoning and response makes this ponderous, it's fine overnight.
40
+ Automated task implementation with Claude Code and Codex for "Ralph Coding". What is [Ralph Coding](https://ghuntley.com/ralph/)? It's a method of coding where context rot is avoided by controlling the retention of information. This method involves re-invoking claude or codex for each task, and passing information about the requirements, acceptance testing, and any progress that's made (or roadblocks/challenges faced) through files, rather than retaining all prompts + thinking + response tokens. It tends to result in more requests, some duplicated token work, but fairly consistent performance, and best of all it can largely be done unattended. Ralph now defaults to continuing past PRD-to-task conversion instead of pausing there, and non-interactive harness calls are bounded by timeouts and turn caps so stuck agent runs fail fast instead of hanging forever. Recommend Claude Max account or codex equivalent, but be aware that GPT-5 - GPT5.2's slow reasoning and response makes this ponderous, it's fine overnight.
41
41
 
42
42
  Because LLMs are carrying out the work, we can specify a job of "Find all the python files in the project that directly or indirectly access sqlalchemy objects, and upgrade the code to work with sqlalchemy 2.* This will result in probably a single-task project, but that one task might add 50 other tasks (on per file) to the backlog, which are then processed sequentially."
43
43
 
@@ -59,6 +59,15 @@ pip install ralph-code
59
59
  ralph [OPTIONS] [DIRECTORY]
60
60
  ```
61
61
 
62
+ ## Recent changes
63
+
64
+ Version `0.6.2` includes:
65
+ - Bounded non-interactive harness execution with timeout and turn limits
66
+ - Structured `tasks.json` generation for more reliable PRD conversion
67
+ - Automatic continuation after task generation by default
68
+ - `PRDs/` as the standard task directory, with legacy `PRD/` compatibility
69
+ - Refreshed model catalogs and current defaults
70
+
62
71
  ### Options
63
72
 
64
73
  - `--debug`: Enable debug logging, logs are saved into the .ralph subdirectory of the project
@@ -66,8 +75,8 @@ ralph [OPTIONS] [DIRECTORY]
66
75
 
67
76
  ## Usage
68
77
 
69
- First create a task, give a short name for the task (used for the branch commits will be added to), and then give a description.
70
- Then you run the ralph-coder, it will produce a .md file of the specifications, which will be broken into small tasks put into a tasks.json file. Each task will be worked on independently.
78
+ First create a task in `PRDs/`, give a short name for the task (used for the branch commits will be added to), and then give a description.
79
+ Then you run `ralph`, it will produce a `.md` file of the specifications, which will be broken into small tasks put into a `tasks.json` file. Each task will be worked on independently.
71
80
 
72
81
  ## Requirements
73
82
 
@@ -1,6 +1,6 @@
1
1
  # ralph-code
2
2
 
3
- Automated task implementation with Claude Code and Codex for "Ralph Coding". What is [Ralph Coding](https://ghuntley.com/ralph/)? It's a method of coding where context rot is avoided by controlling the retention of information. This method involves re-invoking claude or codex for each task, and passing information about the requirements, acceptance testing, and any progress that's made (or roadblocks/challenges faced) through files, rather than retaining all prompts + thinking + response tokens. It tends to result in more requests, some duplicated token work, but fairly consistent performance, and best of all it can largely be done unattended. Recommend Claude Max account or codex equivalent, but be aware that GPT-5 - GPT5.2's slow reasoning and response makes this ponderous, it's fine overnight.
3
+ Automated task implementation with Claude Code and Codex for "Ralph Coding". What is [Ralph Coding](https://ghuntley.com/ralph/)? It's a method of coding where context rot is avoided by controlling the retention of information. This method involves re-invoking claude or codex for each task, and passing information about the requirements, acceptance testing, and any progress that's made (or roadblocks/challenges faced) through files, rather than retaining all prompts + thinking + response tokens. It tends to result in more requests, some duplicated token work, but fairly consistent performance, and best of all it can largely be done unattended. Ralph now defaults to continuing past PRD-to-task conversion instead of pausing there, and non-interactive harness calls are bounded by timeouts and turn caps so stuck agent runs fail fast instead of hanging forever. Recommend Claude Max account or codex equivalent, but be aware that GPT-5 - GPT5.2's slow reasoning and response makes this ponderous, it's fine overnight.
4
4
 
5
5
  Because LLMs are carrying out the work, we can specify a job of "Find all the python files in the project that directly or indirectly access sqlalchemy objects, and upgrade the code to work with sqlalchemy 2.* This will result in probably a single-task project, but that one task might add 50 other tasks (on per file) to the backlog, which are then processed sequentially."
6
6
 
@@ -22,6 +22,15 @@ pip install ralph-code
22
22
  ralph [OPTIONS] [DIRECTORY]
23
23
  ```
24
24
 
25
+ ## Recent changes
26
+
27
+ Version `0.6.2` includes:
28
+ - Bounded non-interactive harness execution with timeout and turn limits
29
+ - Structured `tasks.json` generation for more reliable PRD conversion
30
+ - Automatic continuation after task generation by default
31
+ - `PRDs/` as the standard task directory, with legacy `PRD/` compatibility
32
+ - Refreshed model catalogs and current defaults
33
+
25
34
  ### Options
26
35
 
27
36
  - `--debug`: Enable debug logging, logs are saved into the .ralph subdirectory of the project
@@ -29,8 +38,8 @@ ralph [OPTIONS] [DIRECTORY]
29
38
 
30
39
  ## Usage
31
40
 
32
- First create a task, give a short name for the task (used for the branch commits will be added to), and then give a description.
33
- Then you run the ralph-coder, it will produce a .md file of the specifications, which will be broken into small tasks put into a tasks.json file. Each task will be worked on independently.
41
+ First create a task in `PRDs/`, give a short name for the task (used for the branch commits will be added to), and then give a description.
42
+ Then you run `ralph`, it will produce a `.md` file of the specifications, which will be broken into small tasks put into a `tasks.json` file. Each task will be worked on independently.
34
43
 
35
44
  ## Requirements
36
45
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ralph-code"
7
- version = "0.5.0"
7
+ version = "0.6.2"
8
8
  description = "Automated task implementation with Claude Code and Codex"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -57,6 +57,15 @@ include = ["ralph*"]
57
57
  [tool.setuptools.package-data]
58
58
  ralph = ["schemas/*.json"]
59
59
 
60
+ [tool.pytest.ini_options]
61
+ markers = [
62
+ "integration: marks tests as integration tests (may be slower)",
63
+ "e2e: marks tests as end-to-end tests (requires terminal)",
64
+ "slow: marks tests as slow (> 1 second)",
65
+ ]
66
+ testpaths = ["tests"]
67
+ addopts = "-v"
68
+
60
69
  [tool.mypy]
61
70
  strict = true
62
71
  python_version = "3.12"
@@ -1,6 +1,6 @@
1
1
  """ralph-code: Automated task implementation with Claude Code and Codex."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.6.2"
4
4
  __author__ = "Ralph Coding"
5
5
 
6
6
  from .app import RalphApp, main
@@ -7,8 +7,16 @@ import click
7
7
 
8
8
  from .app import main
9
9
 
10
+ # Version is imported from pyproject.toml at build time
11
+ try:
12
+ from importlib.metadata import version
13
+ __version__ = version("ralph-code")
14
+ except Exception:
15
+ __version__ = "unknown"
16
+
10
17
 
11
18
  @click.command()
19
+ @click.version_option(version=__version__, prog_name="ralph")
12
20
  @click.option(
13
21
  "--debug",
14
22
  is_flag=True,
@@ -353,9 +353,19 @@ class RalphApp:
353
353
  elif workflow.is_paused:
354
354
  choices = [
355
355
  {"name": "Resume workflow", "value": "resume"},
356
+ ]
357
+ # If there's an error, offer retry option
358
+ if workflow.error_message:
359
+ choices.append({"name": "Retry", "value": "retry"})
360
+ # If paused after spec/tasks, offer review options
361
+ if workflow.current_task:
362
+ choices.append({"name": "Review PRD (open in editor)", "value": "review_prd"})
363
+ if has_stories:
364
+ choices.append({"name": "Review tasks.json (open in editor)", "value": "review_tasks"})
365
+ choices.extend([
356
366
  {"name": "Add PRD", "value": "add"},
357
367
  {"name": "List PRDs", "value": "list"},
358
- ]
368
+ ])
359
369
  if has_stories:
360
370
  choices.append({"name": "Manage stories", "value": "stories"})
361
371
  choices.extend([
@@ -440,6 +450,8 @@ class RalphApp:
440
450
  {"name": f"Branch prefix: {self._config.branch_prefix}", "value": "branch_prefix"},
441
451
  {"name": f"On error: {self._config.on_error}", "value": "on_error"},
442
452
  {"name": f"Auto spec: {self._config.auto_spec_without_oversight}", "value": "auto_spec"},
453
+ {"name": f"Pause after PRD spec: {self._config.pause_after_spec}", "value": "pause_after_spec"},
454
+ {"name": f"Pause after tasks: {self._config.pause_after_tasks}", "value": "pause_after_tasks"},
443
455
  {"name": f"Wait on rate limit: {self._config.wait_on_rate_limit}", "value": "rate_limit"},
444
456
  {"name": f"Pause on completion: {self._config.pause_on_completion}", "value": "pause_completion"},
445
457
  {"name": f"Always build tests: {self._config.always_build_tests}", "value": "tests"},
@@ -493,6 +505,12 @@ class RalphApp:
493
505
  elif setting == "auto_spec":
494
506
  self._config.auto_spec_without_oversight = not self._config.auto_spec_without_oversight
495
507
  self.console.print(f"[cyan]Auto spec: {self._config.auto_spec_without_oversight}[/cyan]")
508
+ elif setting == "pause_after_spec":
509
+ self._config.pause_after_spec = not self._config.pause_after_spec
510
+ self.console.print(f"[cyan]Pause after PRD spec: {self._config.pause_after_spec}[/cyan]")
511
+ elif setting == "pause_after_tasks":
512
+ self._config.pause_after_tasks = not self._config.pause_after_tasks
513
+ self.console.print(f"[cyan]Pause after tasks: {self._config.pause_after_tasks}[/cyan]")
496
514
  elif setting == "rate_limit":
497
515
  self._config.wait_on_rate_limit = not self._config.wait_on_rate_limit
498
516
  self.console.print(f"[cyan]Wait on rate limit: {self._config.wait_on_rate_limit}[/cyan]")
@@ -541,7 +559,7 @@ class RalphApp:
541
559
  choices: list[Choice] = []
542
560
 
543
561
  for model_name, label in supported_models:
544
- choices.append(Choice(title=model_name, value=model_name))
562
+ choices.append(Choice(title=f"{model_name} ({label})", value=model_name))
545
563
 
546
564
  # No models available - show alert and return
547
565
  if not choices:
@@ -1057,6 +1075,60 @@ class RalphApp:
1057
1075
  workflow.story_manager.update_story(story)
1058
1076
  self.console.print(f"[{NORD14}]Removed: {removed}[/{NORD14}]")
1059
1077
 
1078
+ def _review_prd(self, workflow: WorkflowEngine) -> None:
1079
+ """Open the current PRD in the default editor."""
1080
+ if not workflow.current_task:
1081
+ self.console.print("[dim]No PRD currently selected.[/dim]")
1082
+ return
1083
+
1084
+ prd_file = workflow.current_task.file_path
1085
+ self.console.print(f"Opening {prd_file} in default editor...")
1086
+
1087
+ import subprocess
1088
+ import platform
1089
+
1090
+ try:
1091
+ if platform.system() == "Darwin": # macOS
1092
+ subprocess.run(["open", str(prd_file)])
1093
+ elif platform.system() == "Windows":
1094
+ subprocess.run(["start", str(prd_file)], shell=True)
1095
+ else: # Linux
1096
+ subprocess.run(["xdg-open", str(prd_file)])
1097
+
1098
+ self.console.print("[green]✓ Opened in editor. Resume workflow when ready.[/green]")
1099
+ # Reload PRD after editing
1100
+ workflow.prd_manager.reload()
1101
+ except Exception as e:
1102
+ self.console.print(f"[red]Failed to open editor: {e}[/red]")
1103
+ self.console.print(f"[dim]File location: {prd_file}[/dim]")
1104
+
1105
+ def _review_tasks(self, workflow: WorkflowEngine) -> None:
1106
+ """Open tasks.json in the default editor."""
1107
+ tasks_file = self.project_dir / "tasks.json"
1108
+ if not tasks_file.exists():
1109
+ self.console.print("[dim]No tasks.json file found.[/dim]")
1110
+ return
1111
+
1112
+ self.console.print(f"Opening {tasks_file} in default editor...")
1113
+
1114
+ import subprocess
1115
+ import platform
1116
+
1117
+ try:
1118
+ if platform.system() == "Darwin": # macOS
1119
+ subprocess.run(["open", str(tasks_file)])
1120
+ elif platform.system() == "Windows":
1121
+ subprocess.run(["start", str(tasks_file)], shell=True)
1122
+ else: # Linux
1123
+ subprocess.run(["xdg-open", str(tasks_file)])
1124
+
1125
+ self.console.print("[green]✓ Opened in editor. Resume workflow when ready.[/green]")
1126
+ # Reload tasks after editing
1127
+ workflow.story_manager.reload()
1128
+ except Exception as e:
1129
+ self.console.print(f"[red]Failed to open editor: {e}[/red]")
1130
+ self.console.print(f"[dim]File location: {tasks_file}[/dim]")
1131
+
1060
1132
  def _show_answer_questions(self) -> None:
1061
1133
  """Show UI to answer PRD questions."""
1062
1134
  workflow = self._get_workflow()
@@ -1167,6 +1239,17 @@ class RalphApp:
1167
1239
  self._show_settings_menu()
1168
1240
  elif choice == "run" or choice == "resume":
1169
1241
  self._run_workflow()
1242
+ elif choice == "retry":
1243
+ # Reset iteration counter and run workflow again
1244
+ # resume() resets iteration to 0, giving full max_iterations again
1245
+ current_max = workflow._config.max_iterations
1246
+ self.console.print(f"[green]Retrying with {current_max} attempts...[/green]")
1247
+ workflow.resume()
1248
+ self._run_workflow()
1249
+ elif choice == "review_prd":
1250
+ self._review_prd(workflow)
1251
+ elif choice == "review_tasks":
1252
+ self._review_tasks(workflow)
1170
1253
  elif choice == "pause":
1171
1254
  workflow.pause()
1172
1255
  self.console.print("[yellow]Paused[/yellow]")
@@ -1295,20 +1378,39 @@ class RalphApp:
1295
1378
  self.console.print()
1296
1379
  self._last_status_key = None # Reset change detection
1297
1380
 
1381
+ live = None
1298
1382
  try:
1299
- # Higher refresh rate for spinner animation
1300
- with Live(self._build_live_status(), refresh_per_second=10, console=self.console) as live:
1301
- self._live = live
1302
- self._spinner.reset()
1303
- while workflow.step():
1304
- # Always update to animate spinner during active states
1305
- live.update(self._build_live_status())
1306
- self._live = None
1383
+ # Moderate refresh rate for spinner animation (4 fps for better responsiveness)
1384
+ live = Live(
1385
+ self._build_live_status(),
1386
+ refresh_per_second=4,
1387
+ console=self.console,
1388
+ transient=False
1389
+ )
1390
+ live.start()
1391
+ self._live = live
1392
+ self._spinner.reset()
1393
+
1394
+ while workflow.step():
1395
+ # Update display for spinner animation
1396
+ live.update(self._build_live_status())
1397
+
1398
+ # Safety check: if workflow is paused or in error state, stop
1399
+ if workflow.is_paused or workflow.state == WorkflowState.ERROR:
1400
+ break
1307
1401
 
1308
1402
  except KeyboardInterrupt:
1309
- self._live = None
1310
1403
  workflow.pause()
1404
+ if live:
1405
+ live.stop()
1406
+ self._live = None
1311
1407
  self.console.print("\n[yellow]Paused by user[/yellow]")
1408
+ return
1409
+
1410
+ finally:
1411
+ if live:
1412
+ live.stop()
1413
+ self._live = None
1312
1414
 
1313
1415
  # Show final status
1314
1416
  if workflow.state == WorkflowState.IDLE:
@@ -14,9 +14,13 @@ DEFAULT_CONFIG = {
14
14
  "harness": "claude",
15
15
  "worker_model": "opus",
16
16
  "summary_model": "haiku",
17
+ "harness_timeout_seconds": 1800,
18
+ "non_interactive_max_turns": 12,
17
19
  "max_iterations": 10,
18
20
  "max_story_attempts": 3,
19
21
  "auto_spec_without_oversight": True,
22
+ "pause_after_spec": False,
23
+ "pause_after_tasks": False,
20
24
  "wait_on_rate_limit": True,
21
25
  "pause_on_completion": True,
22
26
  "always_build_tests": False,
@@ -61,7 +65,7 @@ class Config:
61
65
  if "worker_model" not in self._config:
62
66
  harness = self._config.get("harness", DEFAULT_CONFIG["harness"])
63
67
  if harness == "codex":
64
- self._config["worker_model"] = "gpt-5.2-codex"
68
+ self._config["worker_model"] = "gpt-5.3-codex"
65
69
  else:
66
70
  self._config["worker_model"] = "opus"
67
71
  needs_save = True
@@ -70,7 +74,7 @@ class Config:
70
74
  if "summary_model" not in self._config:
71
75
  harness = self._config.get("harness", DEFAULT_CONFIG["harness"])
72
76
  if harness == "codex":
73
- self._config["summary_model"] = "gpt-5.2"
77
+ self._config["summary_model"] = "gpt-5.1-codex-mini"
74
78
  else:
75
79
  self._config["summary_model"] = "haiku"
76
80
  needs_save = True
@@ -122,6 +126,26 @@ class Config:
122
126
  self._config["summary_model"] = value
123
127
  self._save()
124
128
 
129
+ @property
130
+ def harness_timeout_seconds(self) -> int:
131
+ """Maximum seconds to wait for a harness subprocess before aborting."""
132
+ return int(self._config.get("harness_timeout_seconds", 1800))
133
+
134
+ @harness_timeout_seconds.setter
135
+ def harness_timeout_seconds(self, value: int) -> None:
136
+ self._config["harness_timeout_seconds"] = max(1, value)
137
+ self._save()
138
+
139
+ @property
140
+ def non_interactive_max_turns(self) -> int:
141
+ """Maximum agent turns for non-interactive harness runs."""
142
+ return int(self._config.get("non_interactive_max_turns", 12))
143
+
144
+ @non_interactive_max_turns.setter
145
+ def non_interactive_max_turns(self, value: int) -> None:
146
+ self._config["non_interactive_max_turns"] = max(1, value)
147
+ self._save()
148
+
125
149
  @property
126
150
  def max_iterations(self) -> int:
127
151
  """Maximum iterations for implementation loop."""
@@ -142,6 +166,26 @@ class Config:
142
166
  self._config["auto_spec_without_oversight"] = value
143
167
  self._save()
144
168
 
169
+ @property
170
+ def pause_after_spec(self) -> bool:
171
+ """Whether to pause after creating PRD specs for review."""
172
+ return bool(self._config["pause_after_spec"])
173
+
174
+ @pause_after_spec.setter
175
+ def pause_after_spec(self, value: bool) -> None:
176
+ self._config["pause_after_spec"] = value
177
+ self._save()
178
+
179
+ @property
180
+ def pause_after_tasks(self) -> bool:
181
+ """Whether to pause after converting PRD to tasks for review."""
182
+ return bool(self._config["pause_after_tasks"])
183
+
184
+ @pause_after_tasks.setter
185
+ def pause_after_tasks(self, value: bool) -> None:
186
+ self._config["pause_after_tasks"] = value
187
+ self._save()
188
+
145
189
  @property
146
190
  def wait_on_rate_limit(self) -> bool:
147
191
  """Whether to wait and retry on rate limit."""
@@ -170,8 +170,9 @@ class GitManager:
170
170
  """Get list of changed files that should be staged (excluding ralph files)."""
171
171
  result = self._run_git("status", "--porcelain")
172
172
  files = []
173
- for line in result.stdout.strip().split("\n"):
174
- if not line:
173
+ # Use rstrip() to preserve leading spaces which are significant in porcelain format
174
+ for line in result.stdout.rstrip().split("\n"):
175
+ if not line.strip():
175
176
  continue
176
177
  # Status is first 2 chars, filename starts at position 3
177
178
  filepath = line[3:].strip()
@@ -229,6 +230,11 @@ class GitManager:
229
230
 
230
231
  return self.commit(message)
231
232
 
233
+ def get_staged_files(self) -> list[str]:
234
+ """Get list of files currently staged for commit."""
235
+ result = self._run_git("diff", "--cached", "--name-only")
236
+ return [f for f in result.stdout.strip().split("\n") if f]
237
+
232
238
  def get_unstaged_files(self) -> list[str]:
233
239
  """Get list of files with unstaged changes (modified + untracked).
234
240
 
@@ -46,13 +46,19 @@ HarnessType = Literal["claude", "codex", "custom"]
46
46
  # These defaults are used when CLI model querying fails or isn't supported.
47
47
  DEFAULT_MODELS: dict[HarnessType, list[tuple[str, str]]] = {
48
48
  "claude": [
49
+ ("default", "Standard"),
49
50
  ("haiku", "Light"),
50
51
  ("sonnet", "Standard"),
51
52
  ("opus", "Standard"),
53
+ ("sonnet[1m]", "Extended"),
54
+ ("opusplan", "Planning"),
52
55
  ],
53
56
  "codex": [
54
57
  ("gpt-5.1-codex-mini", "Light"),
58
+ ("gpt-5.3-codex-spark", "Preview"),
59
+ ("gpt-5.3-codex", "Standard"),
55
60
  ("gpt-5.2-codex", "Standard"),
61
+ ("gpt-5.1-codex", "Standard"),
56
62
  ("gpt-5.1-codex-max", "Standard"),
57
63
  ("gpt-5.2", "Standard"),
58
64
  ],
@@ -61,13 +67,13 @@ DEFAULT_MODELS: dict[HarnessType, list[tuple[str, str]]] = {
61
67
 
62
68
  DEFAULT_WORKER_MODEL: dict[HarnessType, str] = {
63
69
  "claude": "opus",
64
- "codex": "gpt-5.2-codex",
70
+ "codex": "gpt-5.3-codex",
65
71
  "custom": "",
66
72
  }
67
73
 
68
74
  DEFAULT_SUMMARY_MODEL: dict[HarnessType, str] = {
69
75
  "claude": "haiku",
70
- "codex": "gpt-5.2",
76
+ "codex": "gpt-5.1-codex-mini",
71
77
  "custom": "",
72
78
  }
73
79