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.
- checksums.yaml +7 -0
- data/.github/workflows/gem-push.yml +47 -0
- data/.gitignore +79 -0
- data/.rubocop.yml +6018 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +113 -0
- data/Gemfile +11 -0
- data/LICENSE +21 -0
- data/README.md +656 -0
- data/bin/rubocop +8 -0
- data/bin/test +5 -0
- data/exe/ralph +8 -0
- data/lib/ralph/agents/base.rb +132 -0
- data/lib/ralph/agents/claude_code.rb +24 -0
- data/lib/ralph/agents/codex.rb +25 -0
- data/lib/ralph/agents/open_code.rb +30 -0
- data/lib/ralph/agents.rb +24 -0
- data/lib/ralph/cli.rb +222 -0
- data/lib/ralph/config.rb +40 -0
- data/lib/ralph/git/file_snapshot.rb +60 -0
- data/lib/ralph/helpers.rb +76 -0
- data/lib/ralph/iteration.rb +220 -0
- data/lib/ralph/loop.rb +196 -0
- data/lib/ralph/output/active_loop_error.rb +13 -0
- data/lib/ralph/output/banner.rb +29 -0
- data/lib/ralph/output/completion_deferred.rb +12 -0
- data/lib/ralph/output/completion_detected.rb +17 -0
- data/lib/ralph/output/config_summary.rb +31 -0
- data/lib/ralph/output/context_consumed.rb +11 -0
- data/lib/ralph/output/iteration.rb +45 -0
- data/lib/ralph/output/max_iterations_reached.rb +16 -0
- data/lib/ralph/output/no_plugin_warning.rb +14 -0
- data/lib/ralph/output/nonzero_exit_warning.rb +11 -0
- data/lib/ralph/output/plugin_error.rb +12 -0
- data/lib/ralph/output/status.rb +176 -0
- data/lib/ralph/output/struggle_warning.rb +18 -0
- data/lib/ralph/output/task_completion.rb +12 -0
- data/lib/ralph/output/tasks_file_created.rb +11 -0
- data/lib/ralph/prompt_template.rb +183 -0
- data/lib/ralph/storage/context.rb +58 -0
- data/lib/ralph/storage/history.rb +117 -0
- data/lib/ralph/storage/state.rb +178 -0
- data/lib/ralph/storage/tasks.rb +244 -0
- data/lib/ralph/threads/heartbeat.rb +44 -0
- data/lib/ralph/threads/stream_reader.rb +50 -0
- data/lib/ralph/version.rb +5 -0
- data/lib/ralph.rb +67 -0
- data/original/bin/ralph.js +13 -0
- data/original/ralph.ts +1706 -0
- data/ralph.gemspec +35 -0
- data/ralph2.gemspec +35 -0
- data/screenshot.webp +0 -0
- data/specs/README.md +46 -0
- data/specs/agents.md +172 -0
- data/specs/cli.md +223 -0
- data/specs/iteration.md +173 -0
- data/specs/output.md +104 -0
- data/specs/storage/local-data-structure.md +246 -0
- data/specs/tasks.md +295 -0
- 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
|
+
```
|
data/specs/iteration.md
ADDED
|
@@ -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.
|