ralph.rb 1.2.435535439

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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/gem-push.yml +47 -0
  3. data/.gitignore +79 -0
  4. data/.rubocop.yml +6018 -0
  5. data/.ruby-version +1 -0
  6. data/AGENTS.md +113 -0
  7. data/Gemfile +11 -0
  8. data/LICENSE +21 -0
  9. data/README.md +656 -0
  10. data/bin/rubocop +8 -0
  11. data/bin/test +5 -0
  12. data/exe/ralph +8 -0
  13. data/lib/ralph/agents/base.rb +132 -0
  14. data/lib/ralph/agents/claude_code.rb +24 -0
  15. data/lib/ralph/agents/codex.rb +25 -0
  16. data/lib/ralph/agents/open_code.rb +30 -0
  17. data/lib/ralph/agents.rb +24 -0
  18. data/lib/ralph/cli.rb +222 -0
  19. data/lib/ralph/config.rb +40 -0
  20. data/lib/ralph/git/file_snapshot.rb +60 -0
  21. data/lib/ralph/helpers.rb +76 -0
  22. data/lib/ralph/iteration.rb +220 -0
  23. data/lib/ralph/loop.rb +196 -0
  24. data/lib/ralph/output/active_loop_error.rb +13 -0
  25. data/lib/ralph/output/banner.rb +29 -0
  26. data/lib/ralph/output/completion_deferred.rb +12 -0
  27. data/lib/ralph/output/completion_detected.rb +17 -0
  28. data/lib/ralph/output/config_summary.rb +31 -0
  29. data/lib/ralph/output/context_consumed.rb +11 -0
  30. data/lib/ralph/output/iteration.rb +45 -0
  31. data/lib/ralph/output/max_iterations_reached.rb +16 -0
  32. data/lib/ralph/output/no_plugin_warning.rb +14 -0
  33. data/lib/ralph/output/nonzero_exit_warning.rb +11 -0
  34. data/lib/ralph/output/plugin_error.rb +12 -0
  35. data/lib/ralph/output/status.rb +176 -0
  36. data/lib/ralph/output/struggle_warning.rb +18 -0
  37. data/lib/ralph/output/task_completion.rb +12 -0
  38. data/lib/ralph/output/tasks_file_created.rb +11 -0
  39. data/lib/ralph/prompt_template.rb +183 -0
  40. data/lib/ralph/storage/context.rb +58 -0
  41. data/lib/ralph/storage/history.rb +117 -0
  42. data/lib/ralph/storage/state.rb +178 -0
  43. data/lib/ralph/storage/tasks.rb +244 -0
  44. data/lib/ralph/threads/heartbeat.rb +44 -0
  45. data/lib/ralph/threads/stream_reader.rb +50 -0
  46. data/lib/ralph/version.rb +5 -0
  47. data/lib/ralph.rb +67 -0
  48. data/original/bin/ralph.js +13 -0
  49. data/original/ralph.ts +1706 -0
  50. data/ralph.gemspec +35 -0
  51. data/ralph2.gemspec +35 -0
  52. data/screenshot.webp +0 -0
  53. data/specs/README.md +46 -0
  54. data/specs/agents.md +172 -0
  55. data/specs/cli.md +223 -0
  56. data/specs/iteration.md +173 -0
  57. data/specs/output.md +104 -0
  58. data/specs/storage/local-data-structure.md +246 -0
  59. data/specs/tasks.md +295 -0
  60. metadata +150 -0
data/ralph.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/ralph/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ralph.rb"
7
+ spec.version = Ralph::VERSION
8
+ spec.authors = ["Nathan Kidd"]
9
+ spec.email = ["nathankidd@hey.com"]
10
+
11
+ spec.summary = "Autonomous agentic loop for Claude Code, Codex & OpenCode"
12
+
13
+ spec.description = <<~DESC
14
+ Ralph Wiggum Loop - Iterative AI development with AI agents.
15
+ An autonomous agentic loop that drives Claude Code, Codex, and OpenCode.
16
+ DESC
17
+
18
+ spec.homepage = "https://github.com/n-at-han-k/ralph.rb"
19
+ spec.license = "MIT"
20
+ spec.required_ruby_version = ">= 3.3"
21
+
22
+ spec.metadata["homepage_uri"] = spec.homepage
23
+ spec.metadata["source_code_uri"] = spec.homepage
24
+ spec.metadata["documentation_uri"] = spec.homepage
25
+ spec.metadata["rubygems_mfa_required"] = "true"
26
+
27
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_development_dependency "minitest", "~> 5.0"
33
+ spec.add_development_dependency "rake", "~> 13.0"
34
+ spec.add_development_dependency "rubocop", "~> 1.21"
35
+ end
data/ralph2.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/ralph/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ralph"
7
+ spec.version = Ralph::VERSION
8
+ spec.authors = ["Nathan Kidd"]
9
+ spec.email = ["nathankidd@hey.com"]
10
+
11
+ spec.summary = "Autonomous agentic loop for Claude Code, Codex & OpenCode"
12
+
13
+ spec.description = <<~DESC
14
+ Ralph Wiggum Loop - Iterative AI development with AI agents.
15
+ An autonomous agentic loop that drives Claude Code, Codex, and OpenCode.
16
+ DESC
17
+
18
+ spec.homepage = "https://github.com/n-at-han-k/ralph.rb"
19
+ spec.license = "MIT"
20
+ spec.required_ruby_version = ">= 3.3"
21
+
22
+ spec.metadata["homepage_uri"] = spec.homepage
23
+ spec.metadata["source_code_uri"] = spec.homepage
24
+ spec.metadata["documentation_uri"] = spec.homepage
25
+ spec.metadata["rubygems_mfa_required"] = "true"
26
+
27
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_development_dependency "minitest", "~> 5.0"
33
+ spec.add_development_dependency "rake", "~> 13.0"
34
+ spec.add_development_dependency "rubocop", "~> 1.21"
35
+ end
data/screenshot.webp ADDED
Binary file
data/specs/README.md ADDED
@@ -0,0 +1,46 @@
1
+ <!--
2
+ Copyright (c) 2025 Nathan Kidd <nathankidd@hey.com>. All rights reserved.
3
+ SPDX-License-Identifier: Proprietary
4
+ -->
5
+
6
+ <!--
7
+ HOW TO MAINTAIN THIS FILE
8
+
9
+ This is the index of all design specifications for Ralph.rb.
10
+ Each row links a spec document to its implementation code and a short purpose summary.
11
+
12
+ When adding a new spec:
13
+ 1. Create the markdown file in this directory (specs/)
14
+ 2. Add a row to the appropriate table section below
15
+ 3. Link the spec file, the code path it describes, and a brief purpose
16
+
17
+ Table format:
18
+ | [spec-name.md](./spec-name.md) | [path/to/code](../path/to/code) | Short description |
19
+
20
+ Use "—" in the Code column if the spec has no implementation yet.
21
+ Group specs under heading sections by domain area.
22
+ -->
23
+
24
+ # Ralph.rb Specifications
25
+
26
+ Design documentation for Ralph.rb, a Ruby CLI that runs iterative AI development loops.
27
+
28
+ ## Core
29
+
30
+ | Spec | Code | Purpose |
31
+ |------|------|---------|
32
+ | [agents.md](./agents.md) | [lib/ralph/agents/](../lib/ralph/agents/) | Agent abstraction: base class, subclasses, CLI resolution, subprocess argument building |
33
+ | [cli.md](./cli.md) | [lib/ralph/cli.rb](../lib/ralph/cli.rb) | CLI options, subcommands, prompt resolution, error handling |
34
+ ## Data Storage
35
+
36
+ | Spec | Code | Purpose |
37
+ |------|------|---------|
38
+ | [storage/local-data-structure.md](./storage/local-data-structure.md) | [lib/ralph/storage/](../lib/ralph/storage/) | Ralph state persistence: .ralph/ directory, storage module architecture, data lifecycle |
39
+ | [tasks.md](./tasks.md) | [lib/ralph/storage/tasks.rb](../lib/ralph/storage/tasks.rb) | Task management: file format, data models, storage, lifecycle in the loop, prompt integration |
40
+
41
+ ## Output
42
+
43
+ | Spec | Code | Purpose |
44
+ |------|------|---------|
45
+ | [output.md](./output.md) | [lib/ralph/output/](../lib/ralph/output/) | Terminal output structure: callable object pattern, channels, formatting conventions |
46
+
data/specs/agents.md ADDED
@@ -0,0 +1,172 @@
1
+ # Agents Specification
2
+
3
+ The `Ralph::Agents` module provides a polymorphic abstraction over the external AI coding CLIs that Ralph can invoke. Each supported agent is a subclass of `Ralph::Agents::Base`, encapsulating its CLI invocation details, output parsing, and validation.
4
+
5
+ ## Namespace & File Layout
6
+
7
+ ```
8
+ lib/ralph/agents/
9
+ base.rb # Ralph::Agents::Base (abstract), module-level resolve/valid_agent_names
10
+ open_code.rb # Ralph::Agents::OpenCode
11
+ claude_code.rb # Ralph::Agents::ClaudeCode
12
+ codex.rb # Ralph::Agents::Codex
13
+ ```
14
+
15
+ Each file defines exactly one class inside `module Ralph; module Agents; ... end; end`, except `base.rb` which also defines the module-level lookup functions and the `AGENT_NAME_MAP` constant.
16
+
17
+ ## Module Interface
18
+
19
+ The `Ralph::Agents` module exposes two module-level functions for agent discovery and resolution.
20
+
21
+ ### `Agents.resolve(name_str) -> Base | nil`
22
+
23
+ Maps a CLI name string to a new agent instance.
24
+
25
+ - **Parameter:** `name_str` — String, one of the keys in `AGENT_NAME_MAP`
26
+ - **Returns:** A new instance of the matching agent subclass, or `nil` if the name is unknown
27
+ - **Consumers:** `Loop#resolve_agent!`, `Output::Status`
28
+
29
+ ### `Agents.valid_agent_names -> Array<String>`
30
+
31
+ Returns the list of accepted CLI agent name strings for option parsing.
32
+
33
+ - **Returns:** `["opencode", "claude-code", "codex"]`
34
+ - **Consumer:** `CLI` (used in OptionParser `--agent` validation and help text)
35
+
36
+ ### `AGENT_NAME_MAP`
37
+
38
+ Frozen hash mapping CLI strings to internal symbols used for subclass dispatch.
39
+
40
+ ```ruby
41
+ AGENT_NAME_MAP = {
42
+ "opencode" => :opencode,
43
+ "claude-code" => :claude_code,
44
+ "codex" => :codex
45
+ }.freeze
46
+ ```
47
+
48
+ ## Base Class
49
+
50
+ `Ralph::Agents::Base` is an abstract class that includes `Ralph::Helpers` and defines the interface every agent subclass must implement.
51
+
52
+ ### Abstract Methods
53
+
54
+ Subclasses must override all of the following. The base implementations raise `NotImplementedError`.
55
+
56
+ | Method | Signature | Returns | Purpose |
57
+ |--------|-----------|---------|---------|
58
+ | `type` | `-> Symbol` | `:opencode`, `:claude_code`, or `:codex` | Internal identifier used for agent-specific branching |
59
+ | `command` | `-> String` | CLI binary name | The executable name looked up on `$PATH` |
60
+ | `config_name` | `-> String` | Human-readable name | Display name used in banners, warnings, status output |
61
+ | `build_args` | `(prompt, model, options) -> Array<String>` | CLI argument array | Constructs the argument list for the agent subprocess |
62
+ | `parse_tool_output` | `(line) -> String or nil` | Tool name or nil | Extracts a tool invocation name from a single output line |
63
+
64
+ ### Concrete Methods
65
+
66
+ | Method | Signature | Returns | Purpose |
67
+ |--------|-----------|---------|---------|
68
+ | `build_env` | `(options) -> Hash<String, String>` | Environment hash | Returns `ENV.to_h.dup`. Subclasses may override to customise the subprocess environment |
69
+ | `validate!` | `-> void` | — | Checks that `command` is found on `$PATH` via `which`. Prints an error to `$stderr` and calls `exit 1` if missing |
70
+
71
+ ### `build_args` Parameters
72
+
73
+ - `prompt` — String, the full prompt text to send to the agent
74
+ - `model` — String, model identifier (may be `nil` or empty to use the agent's default)
75
+ - `options` — Hash with execution options:
76
+ - `:allow_all_permissions` — Boolean, whether to pass auto-approve flags
77
+
78
+ ### `parse_tool_output` Behaviour
79
+
80
+ - Strips ANSI escape sequences from the line via `strip_ansi` (from `Ralph::Helpers`)
81
+ - Applies an agent-specific regex to extract a tool name
82
+ - Returns the tool name `String` if matched, `nil` otherwise
83
+ - **Consumers:** `Iteration#stream_agent` (real-time tool counting), `Helpers.collect_tool_summary_from_text` (post-hoc counting)
84
+
85
+ ## Agent Subclasses
86
+
87
+ ### `Ralph::Agents::OpenCode`
88
+
89
+ | Property | Value |
90
+ |----------|-------|
91
+ | `type` | `:opencode` |
92
+ | `command` | `"opencode"` |
93
+ | `config_name` | `"OpenCode"` |
94
+
95
+ **`build_args` behaviour:**
96
+ 1. Starts with `["run"]`
97
+ 2. Appends `["-m", model]` if model is non-empty
98
+ 3. Appends the prompt as the final argument
99
+
100
+ **`parse_tool_output` regex:** `/^\|\s{2}([A-Za-z0-9_-]+)/`
101
+
102
+ Matches OpenCode's tool output format where tool names appear after a pipe and two spaces at the start of a line.
103
+
104
+ ### `Ralph::Agents::ClaudeCode`
105
+
106
+ | Property | Value |
107
+ |----------|-------|
108
+ | `type` | `:claude_code` |
109
+ | `command` | `"claude"` |
110
+ | `config_name` | `"Claude Code"` |
111
+
112
+ **`build_args` behaviour:**
113
+ 1. Starts with `["-p", prompt]`
114
+ 2. Appends `["--model", model]` if model is non-empty
115
+ 3. Appends `"--dangerously-skip-permissions"` if `options[:allow_all_permissions]` is truthy
116
+
117
+ **`parse_tool_output` regex:** `/(?:Using|Called|Tool:)\s+([A-Za-z0-9_-]+)/i`
118
+
119
+ Matches Claude Code's tool output format where tool names follow "Using", "Called", or "Tool:" prefixes (case-insensitive).
120
+
121
+ ### `Ralph::Agents::Codex`
122
+
123
+ | Property | Value |
124
+ |----------|-------|
125
+ | `type` | `:codex` |
126
+ | `command` | `"codex"` |
127
+ | `config_name` | `"Codex"` |
128
+
129
+ **`build_args` behaviour:**
130
+ 1. Starts with `["exec"]`
131
+ 2. Appends `["--model", model]` if model is non-empty
132
+ 3. Appends `"--full-auto"` if `options[:allow_all_permissions]` is truthy
133
+ 4. Appends the prompt as the final argument
134
+
135
+ **`parse_tool_output` regex:** `/(?:Tool:|Using|Calling|Running)\s+([A-Za-z0-9_-]+)/i`
136
+
137
+ Matches Codex's tool output format where tool names follow "Tool:", "Using", "Calling", or "Running" prefixes (case-insensitive).
138
+
139
+ ## Consumers
140
+
141
+ | Consumer | What it uses | How |
142
+ |----------|-------------|-----|
143
+ | `Loop#resolve_agent!` | `Agents.resolve`, `agent.validate!` | Resolves CLI name to instance, validates binary exists |
144
+ | `Loop#initialize` | `agent.type`, `agent.config_name` | Agent-specific branching (plugin warnings), display name for banner/warnings |
145
+ | `Iteration#execute_agent` | `agent.build_args`, `agent.build_env`, `agent.command` | Constructs and spawns the agent subprocess |
146
+ | `Iteration#stream_agent` | `agent.parse_tool_output` | Real-time tool counting during streaming output |
147
+ | `Helpers.collect_tool_summary_from_text` | `agent.parse_tool_output` | Post-hoc tool counting from buffered output |
148
+ | `Output::Status` | `Agents.resolve`, `agent.config_name` | Display name in `--status` dashboard |
149
+ | `CLI` | `Agents.valid_agent_names` | OptionParser validation and help text for `--agent` flag |
150
+
151
+ ## Adding a New Agent
152
+
153
+ To add support for a new AI coding CLI:
154
+
155
+ 1. Create `lib/ralph/agents/<name>.rb` with a class inheriting from `Base`
156
+ 2. Implement all abstract methods: `type`, `command`, `config_name`, `build_args`, `parse_tool_output`
157
+ 3. Override `build_env` if the agent requires custom environment variables
158
+ 4. Add an entry to `AGENT_NAME_MAP` in `base.rb`
159
+ 5. Add a constructor lambda to the dispatch hash in `Agents.resolve`
160
+
161
+ ## Testing Strategy
162
+
163
+ ### Unit Tests
164
+ - Verify each subclass returns correct `type`, `command`, and `config_name`
165
+ - Test `build_args` with combinations of: empty model, non-empty model, permissions on/off
166
+ - Test `parse_tool_output` with matching lines, non-matching lines, and lines containing ANSI escapes
167
+ - Test `Agents.resolve` returns correct class for each valid name, `nil` for unknown names
168
+ - Test `Agents.valid_agent_names` returns expected list
169
+
170
+ ### Integration Tests
171
+ - Test `validate!` with a mocked `which` that returns nil (expect stderr output and `SystemExit`)
172
+ - Test `validate!` with a mocked `which` that returns a path (expect no error)
data/specs/cli.md ADDED
@@ -0,0 +1,223 @@
1
+ # CLI Specification
2
+
3
+ Ralph is invoked as a single binary with positional arguments, flags, and subcommands.
4
+
5
+ ```
6
+ ralph "<prompt>" [options]
7
+ ralph <subcommand> [args]
8
+ ```
9
+
10
+ ## Prompt Input
11
+
12
+ The primary argument is a prompt string describing the task for the AI agent.
13
+
14
+ | Input method | Syntax | Notes |
15
+ |---|---|---|
16
+ | Inline string | `ralph "Build a REST API"` | One or more positional args joined with spaces |
17
+ | Piped input | `cat prompt.md \| ralph` | Read prompt from stdin via shell pipe |
18
+ | Command substitution | `ralph $(cat prompt.md)` | Expand file contents as arguments |
19
+
20
+ ### Prompt resolution order
21
+
22
+ 1. Join all positional args with spaces.
23
+ 2. If the resolved prompt is empty, exit with an error.
24
+
25
+ ## Loop Options
26
+
27
+ These flags configure the main iteration loop.
28
+
29
+ ### `--agent AGENT`
30
+
31
+ Select the AI agent to use.
32
+
33
+ - **Type:** String, restricted to valid agent names
34
+ - **Valid values:** `opencode`, `claude-code`, `codex`
35
+ - **Default:** `opencode`
36
+ - **Error on invalid value:** Yes (OptionParser rejects with `invalid argument`)
37
+
38
+ ### `--min-iterations N`
39
+
40
+ Minimum number of iterations before a completion promise is accepted.
41
+
42
+ - **Type:** Integer
43
+ - **Default:** `1`
44
+ - **Behavior:** If the agent outputs the completion promise before this many iterations have run, the loop continues anyway.
45
+
46
+ ### `--max-iterations N`
47
+
48
+ Maximum number of iterations before the loop stops automatically.
49
+
50
+ - **Type:** Integer
51
+ - **Default:** `0` (unlimited)
52
+ - **Behavior:** When the iteration counter exceeds this value, the loop stops and clears state.
53
+
54
+ ### Validation
55
+
56
+ `--min-iterations` must not exceed `--max-iterations` (when max > 0). The CLI exits with an error if this constraint is violated.
57
+
58
+ ### `--completion-promise TEXT`
59
+
60
+ The phrase the agent must output (wrapped in `<promise>...</promise>` tags) to signal task completion.
61
+
62
+ - **Type:** String
63
+ - **Default:** `COMPLETE`
64
+
65
+ ### `--tasks`, `-t`
66
+
67
+ Enable Tasks Mode for structured task tracking.
68
+
69
+ - **Type:** Boolean flag
70
+ - **Default:** `false`
71
+ - **Behavior:** When enabled, the loop works through tasks defined in `.ralph/ralph-tasks.md` one at a time. The prompt builder includes task-specific instructions and workflow guidance.
72
+
73
+ ### `--task-promise TEXT`
74
+
75
+ The phrase the agent outputs to signal an individual task is complete (tasks mode only).
76
+
77
+ - **Type:** String
78
+ - **Default:** `READY_FOR_NEXT_TASK`
79
+
80
+ ### `--model MODEL`
81
+
82
+ Model identifier passed to the selected agent.
83
+
84
+ - **Type:** String
85
+ - **Default:** `""` (agent's default)
86
+ - **Agent-specific:** The value is passed directly to the agent CLI (e.g., `--model` for claude-code/codex, `-m` for opencode).
87
+
88
+ ### `--[no-]stream`
89
+
90
+ Control whether agent output is streamed in real-time or buffered.
91
+
92
+ - **Type:** Boolean
93
+ - **Default:** `true` (streaming on)
94
+ - **`--stream`:** Stream stdout/stderr in real-time with tool counting and heartbeat.
95
+ - **`--no-stream`:** Buffer all output, print at end. Tool counts collected post-hoc.
96
+
97
+ ### `--verbose-tools`
98
+
99
+ Print every tool invocation line instead of periodic compact summaries.
100
+
101
+ - **Type:** Boolean flag
102
+ - **Default:** `false`
103
+ - **Behavior:** When off (default), tool lines are suppressed and a compact summary is printed every 3 seconds. When on, every tool line is printed as-is.
104
+
105
+ ### `--no-plugins`
106
+
107
+ Disable non-auth OpenCode plugins for the current run.
108
+
109
+ - **Type:** Boolean flag
110
+ - **Default:** `false`
111
+ - **Agent-specific:** Only affects `opencode`. Prints a warning if used with `claude-code` or `codex`.
112
+ - **Behavior:** Generates a filtered OpenCode config that only includes plugins matching `/auth/i`.
113
+
114
+ ### `--[no-]commit`
115
+
116
+ Control automatic git commits after each iteration.
117
+
118
+ - **Type:** Boolean
119
+ - **Default:** `true` (auto-commit on)
120
+ - **`--commit`:** After each iteration, if `git status --porcelain` shows changes, run `git add -A && git commit -m "Ralph iteration N: work in progress"`.
121
+ - **`--no-commit`:** Skip auto-commits.
122
+
123
+ ### `--[no-]allow-all`
124
+
125
+ Control automatic tool permission approval.
126
+
127
+ - **Type:** Boolean
128
+ - **Default:** `true` (auto-approve on)
129
+ - **`--allow-all`:** Pass permission flags to the agent (`--dangerously-skip-permissions` for claude-code, `--full-auto` for codex, permission config for opencode).
130
+ - **`--no-allow-all`:** Require interactive permission prompts from the agent.
131
+
132
+ ## Subcommands
133
+
134
+ Subcommands are flag-style (prefixed with `--`) and cause the CLI to perform a specific action then exit. They do not start the main loop.
135
+
136
+ ### `--version`, `-v`
137
+
138
+ Print `ralph <VERSION>` and exit 0.
139
+
140
+ ### `--help`, `-h`
141
+
142
+ Print the full help text (generated by OptionParser) and exit 0.
143
+
144
+ ### `--status`
145
+
146
+ Display the current loop status, pending context, and iteration history, then exit 0.
147
+
148
+ - **With `-t` or `--tasks`:** Also display the current task list from `.ralph/ralph-tasks.md`.
149
+ - **When an active loop has `tasks_mode` enabled:** Task list is shown automatically.
150
+ - **Output includes:**
151
+ - Active/inactive status
152
+ - Current iteration, start time, elapsed time
153
+ - Completion promise, agent, model
154
+ - Pending context (if any)
155
+ - Task list with progress (if shown)
156
+ - Last 5 iterations with duration and top tools
157
+ - Struggle indicators (if detected)
158
+
159
+ ### `--add-context TEXT`
160
+
161
+ Append context text to `.ralph/ralph-context.md` for the next iteration.
162
+
163
+ - **Requires:** A text argument (exits with error if missing)
164
+ - **Behavior:** Appends a timestamped section. Creates the file if it doesn't exist. The context is injected into the prompt on the next iteration, then cleared.
165
+
166
+ ### `--clear-context`
167
+
168
+ Delete `.ralph/ralph-context.md` if it exists.
169
+
170
+ ### `--list-tasks`
171
+
172
+ Parse and display tasks from `.ralph/ralph-tasks.md` with numbered indices and status icons.
173
+
174
+ - Exits with a message if no tasks file exists.
175
+
176
+ ### `--add-task DESC`
177
+
178
+ Append a new `- [ ] DESC` line to `.ralph/ralph-tasks.md`.
179
+
180
+ - **Requires:** A description argument.
181
+ - Creates the file with a header if it doesn't exist.
182
+
183
+ ### `--remove-task N`
184
+
185
+ Remove task at 1-based index N (including its subtasks) from `.ralph/ralph-tasks.md`.
186
+
187
+ - **Requires:** An integer argument.
188
+ - **Validation:** Index must be in range `1..task_count`.
189
+ - **Behavior:** Removes the top-level task line and any indented lines immediately following it (subtasks/notes).
190
+
191
+ ## Error Handling
192
+
193
+ | Condition | Behavior |
194
+ |---|---|
195
+ | Unknown option | Exit 1, print message + `Run 'ralph --help'` |
196
+ | Invalid option value (bad agent, non-integer) | Exit 1, OptionParser error message + `Run 'ralph --help'` |
197
+ | No prompt provided | Exit 1, print usage hint |
198
+ | min-iterations > max-iterations | Exit 1, print constraint error |
199
+ | Agent CLI not found in PATH | Exit 1, print which agent is missing |
200
+ | Loop already active | Exit 1, print active loop info and state file path |
201
+ | Fatal error during loop | Exit 1, clear state, print error |
202
+
203
+ ## Options Hash
204
+
205
+ After parsing, the CLI produces an options hash passed to `Ralph::Loop.run`:
206
+
207
+ ```ruby
208
+ {
209
+ prompt: String, # resolved prompt text
210
+ min_iterations: Integer, # >= 1
211
+ max_iterations: Integer, # 0 = unlimited
212
+ completion_promise: String, # default "COMPLETE"
213
+ tasks_mode: Boolean, # default false
214
+ task_promise: String, # default "READY_FOR_NEXT_TASK"
215
+ model: String, # default ""
216
+ chosen_agent: String, # default "opencode"
217
+ auto_commit: Boolean, # default true
218
+ disable_plugins: Boolean, # default false
219
+ allow_all_permissions: Boolean, # default true
220
+ stream_output: Boolean, # default true
221
+ verbose_tools: Boolean, # default false
222
+ }
223
+ ```
@@ -0,0 +1,173 @@
1
+ # Iteration Class Specification
2
+
3
+ ## Purpose
4
+
5
+ The `Iteration` class is responsible for executing a single AI agent iteration and collecting metrics. It handles the low-level execution details while delegating business logic decisions to the caller.
6
+
7
+ ## Location
8
+
9
+ `lib/ralph/iteration.rb`
10
+
11
+ ## Dependencies
12
+
13
+ ```ruby
14
+ require_relative "types"
15
+ require_relative "helpers"
16
+ require_relative "state"
17
+ ```
18
+
19
+ ## Class Interface
20
+
21
+ ### Constructor
22
+
23
+ ```ruby
24
+ def initialize(loop)
25
+ ```
26
+
27
+ **Parameters:**
28
+ - `loop` - `Ralph::Loop` instance. Provides access to:
29
+ - `loop.config` - `Ralph::Config` instance with all execution options (model, stream_output, verbose_tools, allow_all_permissions, disable_plugins, completion_promise, etc.)
30
+ - `loop.agent_config` - `Ralph::Agents::Base` instance providing command, build_args, build_env, etc. (see [agents.md](./agents.md))
31
+
32
+ ### Main Method
33
+
34
+ ```ruby
35
+ def call(prompt, iteration_start:)
36
+ ```
37
+
38
+ **Parameters:**
39
+ - `prompt` - String containing the prompt to send to the agent
40
+ - `iteration_start` - Integer timestamp (ms) when the iteration started
41
+
42
+ **Returns:**
43
+ - `IterationResult` struct with execution data
44
+
45
+ ## Result Type
46
+
47
+ ```ruby
48
+ IterationResult = Struct.new(
49
+ :duration_ms, # Integer - execution duration in milliseconds
50
+ :exit_code, # Integer - agent process exit code
51
+ :stdout_text, # String - agent stdout output
52
+ :stderr_text, # String - agent stderr output
53
+ :tool_counts, # Hash<String, Integer> - tool usage counts
54
+ :files_modified, # Array<String> - list of files changed during iteration
55
+ :completion_detected, # Boolean - whether completion promise was found
56
+ :errors, # Array<String> - extracted errors from output
57
+ :success # Boolean - overall success (no crashes, exit_code == 0)
58
+ )
59
+ ```
60
+
61
+ ## Internal Responsibilities
62
+
63
+ ### 1. Agent Execution
64
+ - Build agent command using `agent_config.build_args` and `agent_config.build_env`
65
+ - Execute agent (streaming or buffered based on options)
66
+ - Capture stdout, stderr, exit code, and tool usage data
67
+ - Handle agent process lifecycle
68
+
69
+ ### 2. Metrics Collection
70
+ - Calculate execution duration from `iteration_start`
71
+ - Extract tool counts from agent output using `agent_config.parse_tool_output`
72
+ - Capture file snapshots before/after execution using `State.capture_file_snapshot`
73
+ - Detect files modified via `State.modified_files_since_snapshot`
74
+
75
+ ### 3. Data Extraction
76
+ - Combine stdout/stderr for analysis
77
+ - Detect completion promise using `::Ralph::Helpers.check_completion`
78
+ - Extract errors from combined output using `::Ralph::Helpers.extract_errors`
79
+ - Determine overall success status
80
+
81
+ ### 4. Result Packaging
82
+ - Assemble all collected data into `IterationResult`
83
+ - Return structured result to caller
84
+
85
+ ## External Dependencies (Provided by Caller)
86
+
87
+ The class expects the caller to handle:
88
+ - **Prompt Building** - Creating the prompt text using `PromptBuilder`
89
+ - **Output Display** - Showing iteration summaries, warnings, etc.
90
+ - **History Management** - Recording iteration data in `RalphHistory`
91
+ - **State Updates** - Managing loop state, iteration counters
92
+ - **Completion Decisions** - Determining whether to continue or break loop
93
+ - **Error Handling** - Deciding how to respond to iteration errors
94
+ - **Post-Processing** - Auto-committing changes, consuming context
95
+
96
+ ## Error Handling
97
+
98
+ The `Iteration` class handles:
99
+ - Process execution errors (captures in result)
100
+ - Stream processing errors (captures in result)
101
+ - File operation errors for snapshots (gracefully degrades)
102
+ - Agent process cleanup if errors occur
103
+
104
+ It does NOT:
105
+ - Display error messages (caller responsibility)
106
+ - Modify loop state (caller responsibility)
107
+ - Make retry decisions (caller responsibility)
108
+
109
+ ## Usage Example
110
+
111
+ ```ruby
112
+ # In Loop class (Loop exposes attr_reader :config, :agent_config)
113
+ iteration = Iteration.new(self)
114
+
115
+ result = iteration.call(full_prompt, iteration_start: start_time)
116
+
117
+ # Caller processes result
118
+ Output::IterationSummary.call(
119
+ iteration: @state.iteration,
120
+ elapsed_ms: result.duration_ms,
121
+ tool_counts: result.tool_counts,
122
+ exit_code: result.exit_code,
123
+ completion_detected: result.completion_detected
124
+ )
125
+
126
+ # Caller updates history
127
+ record_iteration_in_history(result)
128
+
129
+ # Caller decides what to do next
130
+ if result.completion_detected && @state.iteration >= @min_iterations
131
+ # Handle completion
132
+ end
133
+ ```
134
+
135
+ ## Implementation Notes
136
+
137
+ ### Thread Safety
138
+ The class holds a reference to the parent `Loop` instance and mutable streaming state. It is not intended for concurrent use across multiple loops.
139
+
140
+ ### Performance Considerations
141
+ - File snapshot operations should be efficient (git-based)
142
+ - Stream processing should handle large outputs without memory issues
143
+ - Tool counting should be done efficiently via line-by-line processing
144
+
145
+ ### Extensibility
146
+ New config values are automatically available to `Iteration` via `@loop.config` without constructor changes.
147
+ The `IterationResult` struct can be extended with additional fields as needed.
148
+
149
+ ## Testing Strategy
150
+
151
+ ### Unit Tests
152
+ - Mock `Open3` to test agent execution logic
153
+ - Mock `State` methods to test file snapshot logic
154
+ - Test completion detection with various output formats
155
+ - Test error extraction from different types of output
156
+ - Test tool counting with different agent output formats
157
+
158
+ ### Integration Tests
159
+ - Test with actual agent binaries (if available in test environment)
160
+ - Test file snapshot operations with git repositories
161
+ - Test streaming vs buffered output modes
162
+ - Test error scenarios (agent crashes, git operations fail)
163
+
164
+ ## Migration Path
165
+
166
+ 1. Create the new `Iteration` class with this interface
167
+ 2. Update `Loop` class to use `Iteration` for agent execution
168
+ 3. Move iteration-related private methods from `Loop` to `Iteration`
169
+ 4. Update `Loop` to process `IterationResult` instead of handling execution directly
170
+ 5. Remove redundant code from `Loop` class
171
+ 6. Add tests for the new `Iteration` class
172
+
173
+ This refactoring will improve separation of concerns, testability, and maintainability of the iteration execution logic.