ralph.rb 1.2.435535439 → 2.0.0

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/gem-push.yml +2 -2
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +53 -0
  5. data/lib/ralph/cli.rb +67 -186
  6. data/lib/ralph/display.rb +105 -0
  7. data/lib/ralph/events.rb +117 -0
  8. data/lib/ralph/loop.rb +113 -170
  9. data/lib/ralph/metrics.rb +88 -0
  10. data/lib/ralph/opencode.rb +66 -0
  11. data/lib/ralph/version.rb +1 -1
  12. data/lib/ralph.rb +0 -3
  13. data/plans/00-complete-implementation.md +120 -0
  14. data/plans/01-cli-implementation.md +53 -0
  15. data/plans/02-loop-implementation.md +78 -0
  16. data/plans/03-agents-implementation.md +76 -0
  17. data/plans/04-metrics-implementation.md +98 -0
  18. data/plans/README.md +63 -0
  19. data/specs/README.md +4 -15
  20. data/specs/__templates__/API_TEMPLATE.md +0 -0
  21. data/specs/__templates__/AUTOMATION_ACTION_TEMPLATE.md +0 -0
  22. data/specs/__templates__/AUTOMATION_TRIGGER_TEMPLATE.md +0 -0
  23. data/specs/__templates__/CONTROLLER_TEMPLATE.md +32 -0
  24. data/specs/__templates__/INTEGRATION_TEMPLATE.md +0 -0
  25. data/specs/__templates__/MODEL_TEMPLATE.md +0 -0
  26. data/specs/agents.md +426 -120
  27. data/specs/cli.md +11 -218
  28. data/specs/lib/todo_item.rb +144 -0
  29. data/specs/log +15 -0
  30. data/specs/loop.md +42 -0
  31. data/specs/metrics.md +51 -0
  32. metadata +23 -39
  33. data/lib/ralph/agents/base.rb +0 -132
  34. data/lib/ralph/agents/claude_code.rb +0 -24
  35. data/lib/ralph/agents/codex.rb +0 -25
  36. data/lib/ralph/agents/open_code.rb +0 -30
  37. data/lib/ralph/agents.rb +0 -24
  38. data/lib/ralph/config.rb +0 -40
  39. data/lib/ralph/git/file_snapshot.rb +0 -60
  40. data/lib/ralph/helpers.rb +0 -76
  41. data/lib/ralph/iteration.rb +0 -220
  42. data/lib/ralph/output/active_loop_error.rb +0 -13
  43. data/lib/ralph/output/banner.rb +0 -29
  44. data/lib/ralph/output/completion_deferred.rb +0 -12
  45. data/lib/ralph/output/completion_detected.rb +0 -17
  46. data/lib/ralph/output/config_summary.rb +0 -31
  47. data/lib/ralph/output/context_consumed.rb +0 -11
  48. data/lib/ralph/output/iteration.rb +0 -45
  49. data/lib/ralph/output/max_iterations_reached.rb +0 -16
  50. data/lib/ralph/output/no_plugin_warning.rb +0 -14
  51. data/lib/ralph/output/nonzero_exit_warning.rb +0 -11
  52. data/lib/ralph/output/plugin_error.rb +0 -12
  53. data/lib/ralph/output/status.rb +0 -176
  54. data/lib/ralph/output/struggle_warning.rb +0 -18
  55. data/lib/ralph/output/task_completion.rb +0 -12
  56. data/lib/ralph/output/tasks_file_created.rb +0 -11
  57. data/lib/ralph/prompt_template.rb +0 -183
  58. data/lib/ralph/storage/context.rb +0 -58
  59. data/lib/ralph/storage/history.rb +0 -117
  60. data/lib/ralph/storage/state.rb +0 -178
  61. data/lib/ralph/storage/tasks.rb +0 -244
  62. data/lib/ralph/threads/heartbeat.rb +0 -44
  63. data/lib/ralph/threads/stream_reader.rb +0 -50
  64. data/original/bin/ralph.js +0 -13
  65. data/original/ralph.ts +0 -1706
  66. data/specs/iteration.md +0 -173
  67. data/specs/output.md +0 -104
  68. data/specs/storage/local-data-structure.md +0 -246
  69. data/specs/tasks.md +0 -295
data/specs/cli.md CHANGED
@@ -1,223 +1,16 @@
1
- # CLI Specification
1
+ # CLI specification
2
2
 
3
- Ralph is invoked as a single binary with positional arguments, flags, and subcommands.
3
+ How will we interact with the program?
4
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
5
+ ## overview
204
6
 
205
- After parsing, the CLI produces an options hash passed to `Ralph::Loop.run`:
7
+ Keep it simple stupid!! Unix is king, and we live by their philosophy
206
8
 
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
- }
9
+ ```sh
10
+ cat my-prompt-file.md | ralph "more prompt details" \
11
+ --model=opus-4.5 \
12
+ --max-iterations=10 \
13
+ --duration=10 \
14
+ --max-context=80000 \
15
+ --completion="<promise>COMPLETE</promise>"
223
16
  ```
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2025 Nathan Kidd <nathankidd@hey.com>. All rights reserved.
4
+ # SPDX-License-Identifier: Proprietary
5
+
6
+ require 'json'
7
+ require 'time'
8
+
9
+ # Represents a single todo item with full feature support
10
+ class TodoItem
11
+ attr_reader :id, :title, :description, :created_at, :updated_at
12
+ attr_accessor :completed, :priority, :due_date, :tags, :category
13
+
14
+ PRIORITIES = { low: 1, medium: 2, high: 3, urgent: 4 }.freeze
15
+
16
+ def initialize(title:, description: nil, priority: :medium, due_date: nil, tags: [], category: nil, id: nil)
17
+ @id = id || generate_id
18
+ @title = title
19
+ @description = description
20
+ @completed = false
21
+ @priority = priority
22
+ @due_date = due_date
23
+ @tags = tags || []
24
+ @category = category
25
+ @created_at = Time.now
26
+ @updated_at = Time.now
27
+ end
28
+
29
+ def complete!
30
+ if @completed
31
+ @completed = false
32
+ else
33
+ @completed = true
34
+ end
35
+ @updated_at = Time.now
36
+ end
37
+
38
+ def completed?
39
+ @completed
40
+ end
41
+
42
+ def overdue?
43
+ if @due_date && !@completed
44
+ Time.parse(@due_date) < Time.now
45
+ else
46
+ false
47
+ end
48
+ end
49
+
50
+ def due_soon?(hours: 24)
51
+ if @due_date && !@completed
52
+ due_time = Time.parse(@due_date)
53
+ due_time > Time.now && due_time < Time.now + (hours * 3600)
54
+ else
55
+ false
56
+ end
57
+ end
58
+
59
+ def priority_value
60
+ PRIORITIES[@priority] || PRIORITIES[:medium]
61
+ end
62
+
63
+ def add_tag(tag)
64
+ unless @tags.include?(tag)
65
+ @tags << tag
66
+ @updated_at = Time.now
67
+ end
68
+ end
69
+
70
+ def remove_tag(tag)
71
+ if @tags.delete(tag)
72
+ @updated_at = Time.now
73
+ end
74
+ end
75
+
76
+ def matches_filter?(query: nil, tags: nil, category: nil, completed: nil, priority: nil)
77
+ if query && !matches_query?(query)
78
+ false
79
+ elsif tags && (tags & @tags).empty?
80
+ false
81
+ elsif category && @category != category
82
+ false
83
+ elsif !completed.nil? && @completed != completed
84
+ false
85
+ elsif priority && @priority != priority
86
+ false
87
+ else
88
+ true
89
+ end
90
+ end
91
+
92
+ def to_h
93
+ {
94
+ id: @id,
95
+ title: @title,
96
+ description: @description,
97
+ completed: @completed,
98
+ priority: @priority,
99
+ due_date: @due_date,
100
+ tags: @tags,
101
+ category: @category,
102
+ created_at: @created_at.iso8601,
103
+ updated_at: @updated_at.iso8601
104
+ }
105
+ end
106
+
107
+ def self.from_h(hash)
108
+ new(
109
+ id: hash['id'],
110
+ title: hash['title'],
111
+ description: hash['description'],
112
+ priority: hash['priority']&.to_sym || :medium,
113
+ due_date: hash['due_date'],
114
+ tags: hash['tags'] || [],
115
+ category: hash['category']
116
+ ).then do |item|
117
+ item.instance_variable_set(:@completed, hash['completed'] || false)
118
+ item.instance_variable_set(:@created_at, Time.parse(hash['created_at']))
119
+ item.instance_variable_set(:@updated_at, Time.parse(hash['updated_at']))
120
+ item
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ def generate_id
127
+ "#{Time.now.to_i}-#{rand(10000)}"
128
+ end
129
+
130
+ def matches_query?(query)
131
+ query_lower = query.downcase
132
+ if @title.downcase.include?(query_lower)
133
+ true
134
+ elsif @description && @description.downcase.include?(query_lower)
135
+ true
136
+ elsif @tags.any? { |tag| tag.downcase.include?(query_lower) }
137
+ true
138
+ elsif @category && @category.downcase.include?(query_lower)
139
+ true
140
+ else
141
+ false
142
+ end
143
+ end
144
+ end
data/specs/log ADDED
@@ -0,0 +1,15 @@
1
+ {"type":"step_start","timestamp":1770187082911,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f4c89001ZPPQ0fLyoVyI6R","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f17af001hBx2ZnI5s2T7X6","type":"step-start","snapshot":"b743fc3bfaf58816da3d0e322f5af92274b3318c"}}
2
+ {"type":"text","timestamp":1770187082912,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f4c8b001bqAW435IjTqIWL","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f17af001hBx2ZnI5s2T7X6","type":"text","text":"I'll build you a comprehensive super todo app! Let me create a solid Ruby implementation with all the features you'd want.","time":{"start":1770187082902,"end":1770187082902}}}
3
+ {"type":"tool_use","timestamp":1770187087186,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f4c970017d4K2HeeM44TrY","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f17af001hBx2ZnI5s2T7X6","type":"tool","callID":"toolu_013ZVQg5YJHMYqKHav9rKXUu","tool":"todowrite","state":{"status":"completed","input":{"todos":[{"id":"1","content":"Design the todo app architecture and data model","status":"in_progress","priority":"high"},{"id":"2","content":"Implement core Todo and TodoList classes","status":"pending","priority":"high"},{"id":"3","content":"Add persistence layer (JSON file storage)","status":"pending","priority":"high"},{"id":"4","content":"Create CLI interface with command handling","status":"pending","priority":"high"},{"id":"5","content":"Add filtering and search capabilities","status":"pending","priority":"medium"},{"id":"6","content":"Implement priority levels and due dates","status":"pending","priority":"medium"},{"id":"7","content":"Add tags and categories","status":"pending","priority":"medium"},{"id":"8","content":"Create pretty output formatting","status":"pending","priority":"medium"},{"id":"9","content":"Write comprehensive tests","status":"pending","priority":"high"},{"id":"10","content":"Add README with usage examples","status":"pending","priority":"low"}]},"output":"[\n {\n \"id\": \"1\",\n \"content\": \"Design the todo app architecture and data model\",\n \"status\": \"in_progress\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"2\",\n \"content\": \"Implement core Todo and TodoList classes\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"3\",\n \"content\": \"Add persistence layer (JSON file storage)\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"4\",\n \"content\": \"Create CLI interface with command handling\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"5\",\n \"content\": \"Add filtering and search capabilities\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"6\",\n \"content\": \"Implement priority levels and due dates\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"7\",\n \"content\": \"Add tags and categories\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"8\",\n \"content\": \"Create pretty output formatting\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"9\",\n \"content\": \"Write comprehensive tests\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"10\",\n \"content\": \"Add README with usage examples\",\n \"status\": \"pending\",\n \"priority\": \"low\"\n }\n]","title":"10 todos","metadata":{"todos":[{"id":"1","content":"Design the todo app architecture and data model","status":"in_progress","priority":"high"},{"id":"2","content":"Implement core Todo and TodoList classes","status":"pending","priority":"high"},{"id":"3","content":"Add persistence layer (JSON file storage)","status":"pending","priority":"high"},{"id":"4","content":"Create CLI interface with command handling","status":"pending","priority":"high"},{"id":"5","content":"Add filtering and search capabilities","status":"pending","priority":"medium"},{"id":"6","content":"Implement priority levels and due dates","status":"pending","priority":"medium"},{"id":"7","content":"Add tags and categories","status":"pending","priority":"medium"},{"id":"8","content":"Create pretty output formatting","status":"pending","priority":"medium"},{"id":"9","content":"Write comprehensive tests","status":"pending","priority":"high"},{"id":"10","content":"Add README with usage examples","status":"pending","priority":"low"}],"truncated":false},"time":{"start":1770187087172,"end":1770187087180}}}}
4
+ {"type":"step_finish","timestamp":1770187087202,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f5d51001K0X6r5is6bSpCF","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f17af001hBx2ZnI5s2T7X6","type":"step-finish","reason":"tool-calls","snapshot":"a57194ef5a651225fcd30a8c1693512e5ac64d59","cost":0,"tokens":{"input":2,"output":392,"reasoning":0,"cache":{"read":0,"write":15463}}}}
5
+ {"type":"step_start","timestamp":1770187100723,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f9230001va5bdY1KhjxQG0","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f5d7a001ahslHtCQnw7J1X","type":"step-start","snapshot":"cdd7b1abdd3f43bd9efb116cffba98955ba57a2e"}}
6
+ {"type":"tool_use","timestamp":1770187100734,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f9234001Uu930gEHD2BlmN","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f5d7a001ahslHtCQnw7J1X","type":"tool","callID":"toolu_01EYZZEC8oyTLrULZrUsUEx2","tool":"read","state":{"status":"completed","input":{"filePath":"/home/nathan/src/ralph.rb/specs/README.md"},"output":"<file>\n00001| <!--\n00002| Copyright (c) 2025 Nathan Kidd <nathankidd@hey.com>. All rights reserved.\n00003| SPDX-License-Identifier: Proprietary\n00004| -->\n00005| \n00006| <!--\n00007| HOW TO MAINTAIN THIS FILE\n00008| \n00009| This is the index of all design specifications for Ralph.rb.\n00010| Each row links a spec document to its implementation code and a short purpose summary.\n00011| \n00012| When adding a new spec:\n00013| 1. Create the markdown file in this directory (specs/)\n00014| 2. Add a row to the appropriate table section below\n00015| 3. Link the spec file, the code path it describes, and a brief purpose\n00016| \n00017| Table format:\n00018| | [spec-name.md](./spec-name.md) | [path/to/code](../path/to/code) | Short description |\n00019| \n00020| Use \"—\" in the Code column if the spec has no implementation yet.\n00021| Group specs under heading sections by domain area.\n00022| -->\n00023| \n00024| # Ralph.rb Specifications\n00025| \n00026| Design documentation for Ralph.rb, a Ruby CLI that runs iterative AI development loops.\n00027| \n00028| ## Core\n00029| \n00030| | Spec | Code | Purpose |\n00031| |------|------|---------|\n00032| \n\n(End of file - total 32 lines)\n</file>","title":"specs/README.md","metadata":{"preview":"<!--\n Copyright (c) 2025 Nathan Kidd <nathankidd@hey.com>. All rights reserved.\n SPDX-License-Identifier: Proprietary\n-->\n\n<!--\n HOW TO MAINTAIN THIS FILE\n\n This is the index of all design specifications for Ralph.rb.\n Each row links a spec document to its implementation code and a short purpose summary.\n\n When adding a new spec:\n 1. Create the markdown file in this directory (specs/)\n 2. Add a row to the appropriate table section below\n 3. Link the spec file, the code path it describes, and a brief purpose\n\n Table format:\n | [spec-name.md](./spec-name.md) | [path/to/code](../path/to/code) | Short description |\n\n Use \"—\" in the Code column if the spec has no implementation yet.","truncated":false},"time":{"start":1770187100725,"end":1770187100727}}}}
7
+ {"type":"tool_use","timestamp":1770187100734,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f92310016vUTiBTRHwDM5W","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f5d7a001ahslHtCQnw7J1X","type":"tool","callID":"toolu_013K7yZnRe7rxGp6McUPRv6c","tool":"glob","state":{"status":"completed","input":{"pattern":"**/*.rb"},"output":"No files found","title":"specs","metadata":{"count":0,"truncated":false},"time":{"start":1770187100722,"end":1770187100727}}}}
8
+ {"type":"step_finish","timestamp":1770187100738,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f9238001Bc7W61klSlCgMS","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f5d7a001ahslHtCQnw7J1X","type":"step-finish","reason":"tool-calls","snapshot":"9ff571000c58ef49757c25c596933e95d027b68a","cost":0,"tokens":{"input":5,"output":109,"reasoning":0,"cache":{"read":15463,"write":820}}}}
9
+ {"type":"step_start","timestamp":1770187103298,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f9c3f00174ayQ55QZMnCx1","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f9253001aLp1tmXMfkYWJP","type":"step-start","snapshot":"11b5b96ce076193e7d1845670a5bf54dc1dd710e"}}
10
+ {"type":"tool_use","timestamp":1770187105724,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f9c7100124pI64UBSSh46E","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f9253001aLp1tmXMfkYWJP","type":"tool","callID":"toolu_01FraqehJ4VqbchH6Yg9xCyB","tool":"todowrite","state":{"status":"completed","input":{"todos":[{"id":"1","content":"Design the todo app architecture and data model","status":"completed","priority":"high"},{"id":"2","content":"Implement core Todo and TodoList classes","status":"in_progress","priority":"high"},{"id":"3","content":"Add persistence layer (JSON file storage)","status":"pending","priority":"high"},{"id":"4","content":"Create CLI interface with command handling","status":"pending","priority":"high"},{"id":"5","content":"Add filtering and search capabilities","status":"pending","priority":"medium"},{"id":"6","content":"Implement priority levels and due dates","status":"pending","priority":"medium"},{"id":"7","content":"Add tags and categories","status":"pending","priority":"medium"},{"id":"8","content":"Create pretty output formatting","status":"pending","priority":"medium"},{"id":"9","content":"Write comprehensive tests","status":"pending","priority":"high"},{"id":"10","content":"Add README with usage examples","status":"pending","priority":"low"}]},"output":"[\n {\n \"id\": \"1\",\n \"content\": \"Design the todo app architecture and data model\",\n \"status\": \"completed\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"2\",\n \"content\": \"Implement core Todo and TodoList classes\",\n \"status\": \"in_progress\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"3\",\n \"content\": \"Add persistence layer (JSON file storage)\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"4\",\n \"content\": \"Create CLI interface with command handling\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"5\",\n \"content\": \"Add filtering and search capabilities\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"6\",\n \"content\": \"Implement priority levels and due dates\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"7\",\n \"content\": \"Add tags and categories\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"8\",\n \"content\": \"Create pretty output formatting\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"9\",\n \"content\": \"Write comprehensive tests\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"10\",\n \"content\": \"Add README with usage examples\",\n \"status\": \"pending\",\n \"priority\": \"low\"\n }\n]","title":"9 todos","metadata":{"todos":[{"id":"1","content":"Design the todo app architecture and data model","status":"completed","priority":"high"},{"id":"2","content":"Implement core Todo and TodoList classes","status":"in_progress","priority":"high"},{"id":"3","content":"Add persistence layer (JSON file storage)","status":"pending","priority":"high"},{"id":"4","content":"Create CLI interface with command handling","status":"pending","priority":"high"},{"id":"5","content":"Add filtering and search capabilities","status":"pending","priority":"medium"},{"id":"6","content":"Implement priority levels and due dates","status":"pending","priority":"medium"},{"id":"7","content":"Add tags and categories","status":"pending","priority":"medium"},{"id":"8","content":"Create pretty output formatting","status":"pending","priority":"medium"},{"id":"9","content":"Write comprehensive tests","status":"pending","priority":"high"},{"id":"10","content":"Add README with usage examples","status":"pending","priority":"low"}],"truncated":false},"time":{"start":1770187105711,"end":1770187105720}}}}
11
+ {"type":"step_finish","timestamp":1770187105737,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275fa5ba001LZse64oqhRnWw6","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f9253001aLp1tmXMfkYWJP","type":"step-finish","reason":"tool-calls","snapshot":"0012aee2405832cd50eb58cf61f9d6a06d5ec8d7","cost":0,"tokens":{"input":6,"output":354,"reasoning":0,"cache":{"read":16283,"write":575}}}}
12
+ {"type":"step_start","timestamp":1770187107336,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275fac07001OyYeVzom3wXWKJ","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275fa5de001g6TZgrYArTMYDj","type":"step-start","snapshot":"6a96baea47fc3070e5cca8f349aa76d459448b35"}}
13
+ {"type":"tool_use","timestamp":1770187123469,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275fac08001jiLorarTEUEKFX","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275fa5de001g6TZgrYArTMYDj","type":"tool","callID":"toolu_014EWUsB3p8FPzhZJEW2HWPx","tool":"write","state":{"status":"completed","input":{"filePath":"/home/nathan/src/ralph.rb/specs/lib/todo_item.rb","content":"# frozen_string_literal: true\n\n# Copyright (c) 2025 Nathan Kidd <nathankidd@hey.com>. All rights reserved.\n# SPDX-License-Identifier: Proprietary\n\nrequire 'json'\nrequire 'time'\n\n# Represents a single todo item with full feature support\nclass TodoItem\n attr_reader :id, :title, :description, :created_at, :updated_at\n attr_accessor :completed, :priority, :due_date, :tags, :category\n\n PRIORITIES = { low: 1, medium: 2, high: 3, urgent: 4 }.freeze\n\n def initialize(title:, description: nil, priority: :medium, due_date: nil, tags: [], category: nil, id: nil)\n @id = id || generate_id\n @title = title\n @description = description\n @completed = false\n @priority = priority\n @due_date = due_date\n @tags = tags || []\n @category = category\n @created_at = Time.now\n @updated_at = Time.now\n end\n\n def complete!\n if @completed\n @completed = false\n else\n @completed = true\n end\n @updated_at = Time.now\n end\n\n def completed?\n @completed\n end\n\n def overdue?\n if @due_date && !@completed\n Time.parse(@due_date) < Time.now\n else\n false\n end\n end\n\n def due_soon?(hours: 24)\n if @due_date && !@completed\n due_time = Time.parse(@due_date)\n due_time > Time.now && due_time < Time.now + (hours * 3600)\n else\n false\n end\n end\n\n def priority_value\n PRIORITIES[@priority] || PRIORITIES[:medium]\n end\n\n def add_tag(tag)\n unless @tags.include?(tag)\n @tags << tag\n @updated_at = Time.now\n end\n end\n\n def remove_tag(tag)\n if @tags.delete(tag)\n @updated_at = Time.now\n end\n end\n\n def matches_filter?(query: nil, tags: nil, category: nil, completed: nil, priority: nil)\n if query && !matches_query?(query)\n false\n elsif tags && (tags & @tags).empty?\n false\n elsif category && @category != category\n false\n elsif !completed.nil? && @completed != completed\n false\n elsif priority && @priority != priority\n false\n else\n true\n end\n end\n\n def to_h\n {\n id: @id,\n title: @title,\n description: @description,\n completed: @completed,\n priority: @priority,\n due_date: @due_date,\n tags: @tags,\n category: @category,\n created_at: @created_at.iso8601,\n updated_at: @updated_at.iso8601\n }\n end\n\n def self.from_h(hash)\n new(\n id: hash['id'],\n title: hash['title'],\n description: hash['description'],\n priority: hash['priority']&.to_sym || :medium,\n due_date: hash['due_date'],\n tags: hash['tags'] || [],\n category: hash['category']\n ).then do |item|\n item.instance_variable_set(:@completed, hash['completed'] || false)\n item.instance_variable_set(:@created_at, Time.parse(hash['created_at']))\n item.instance_variable_set(:@updated_at, Time.parse(hash['updated_at']))\n item\n end\n end\n\n private\n\n def generate_id\n \"#{Time.now.to_i}-#{rand(10000)}\"\n end\n\n def matches_query?(query)\n query_lower = query.downcase\n if @title.downcase.include?(query_lower)\n true\n elsif @description && @description.downcase.include?(query_lower)\n true\n elsif @tags.any? { |tag| tag.downcase.include?(query_lower) }\n true\n elsif @category && @category.downcase.include?(query_lower)\n true\n else\n false\n end\n end\nend\n"},"output":"","title":"specs/lib/todo_item.rb","metadata":{"diagnostics":{"/home/nathan/src/ralph.rb/specs/lib/todo_item.rb":[]},"filepath":"/home/nathan/src/ralph.rb/specs/lib/todo_item.rb","exists":false,"truncated":false},"time":{"start":1770187120035,"end":1770187123465}}}}
14
+ {"type":"step_finish","timestamp":1770187123482,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275feb0b001BNSMKEYbJq0b91","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275fa5de001g6TZgrYArTMYDj","type":"step-finish","reason":"tool-calls","snapshot":"177366b69da91d17a347fe0ad0f65091e4bea58d","cost":0,"tokens":{"input":5,"output":1164,"reasoning":0,"cache":{"read":16858,"write":798}}}}
15
+ {"type":"step_start","timestamp":1770187125160,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275ff1a60017BYnKNMjfD21UA","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275feb31001x6mnGGa2J1HWi5","type":"step-start","snapshot":"9edefc0011373084034390a13e04d98067bc123b"}}
data/specs/loop.md ADDED
@@ -0,0 +1,42 @@
1
+ # Loop Specification
2
+
3
+ What is a ralph loop, and how do we implement one?
4
+
5
+ ## Overview
6
+
7
+ The essence of a ralph loop is an infinite loop will certain guards to prevent context becoming tainted. If the context becomes too large then then the iteration is cancelled and started again. The work done is not lost, it just means that a fresh context will carry on the work.
8
+
9
+ This whole technique relies on a way of constantly reminding the agent what it's working on and what it's done. This is simply ensured by a task list. It will be passed the task list as a prompt, and will work until it thinkgs it has completed the task, or it is cancelled by the loop.
10
+
11
+ ## Architecture
12
+
13
+ A main loop simply runs contonuously to read data from opencode, and check whether the iteration needs cancelling.
14
+
15
+ An iteration object is created which run the agent, and can be cancelled whenever we like.
16
+
17
+ In order for the loop to stop once complete, we must instruct the agent to pass back a certain string that we will detect when it thinks it has completed.
18
+
19
+ ### What ends the loop iteration
20
+ - iteration context length
21
+ - iteration duration
22
+
23
+ ### What ends the main loop
24
+ - complete string received
25
+ - max iteration reached
26
+ - loop duration
27
+
28
+ ## Prompts
29
+ Only prompt needed is to explain to return the string once completed, or perhaps to follow the items on the list. The rest can be passed in by the user.
30
+
31
+ **important** WE MUST INSTRUCT the agent NOT to interact with the user. All questions must be aswered by reading the spec.
32
+
33
+ ## Display Output
34
+
35
+ We want to see the following:
36
+ - number of iterations
37
+ - current iteration
38
+ - duration
39
+ - tokens consumed
40
+ - over all and per iteration data
41
+ - the output of the agent
42
+ - the input prompt
data/specs/metrics.md ADDED
@@ -0,0 +1,51 @@
1
+ # Metrics specification
2
+
3
+ How will we get essential data such as current context?
4
+
5
+ ## Overview
6
+
7
+ JSON streaming allows us to do this.
8
+
9
+ ## Architecture
10
+
11
+ Here's an example JSON stream from opencode. You will read the stream and parse the context from it so that the loop can perform it's checks.
12
+ An Events.rb object will contain classes representing each event we need to parse.
13
+
14
+ ```
15
+ {"type":"step_start","timestamp":1770187082911,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f4c89001ZPPQ0fLyoVyI6R","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f17af001hBx2ZnI5s2T7X6","type":"step-start","snapshot":"b743fc3bfaf58816da3d0e322f5af92274b3318c"}}
16
+ {"type":"text","timestamp":1770187082912,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f4c8b001bqAW435IjTqIWL","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f17af001hBx2ZnI5s2T7X6","type":"text","text":"I'll build you a comprehensive super todo app! Let me create a solid Ruby implementation with all the features you'd want.","time":{"start":1770187082902,"end":1770187082902}}}
17
+ {"type":"tool_use","timestamp":1770187087186,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f4c970017d4K2HeeM44TrY","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f17af001hBx2ZnI5s2T7X6","type":"tool","callID":"toolu_013ZVQg5YJHMYqKHav9rKXUu","tool":"todowrite","state":{"status":"completed","input":{"todos":[{"id":"1","content":"Design the todo app architecture and data model","status":"in_progress","priority":"high"},{"id":"2","content":"Implement core Todo and TodoList classes","status":"pending","priority":"high"},{"id":"3","content":"Add persistence layer (JSON file storage)","status":"pending","priority":"high"},{"id":"4","content":"Create CLI interface with command handling","status":"pending","priority":"high"},{"id":"5","content":"Add filtering and search capabilities","status":"pending","priority":"medium"},{"id":"6","content":"Implement priority levels and due dates","status":"pending","priority":"medium"},{"id":"7","content":"Add tags and categories","status":"pending","priority":"medium"},{"id":"8","content":"Create pretty output formatting","status":"pending","priority":"medium"},{"id":"9","content":"Write comprehensive tests","status":"pending","priority":"high"},{"id":"10","content":"Add README with usage examples","status":"pending","priority":"low"}]},"output":"[\n {\n \"id\": \"1\",\n \"content\": \"Design the todo app architecture and data model\",\n \"status\": \"in_progress\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"2\",\n \"content\": \"Implement core Todo and TodoList classes\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"3\",\n \"content\": \"Add persistence layer (JSON file storage)\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"4\",\n \"content\": \"Create CLI interface with command handling\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"5\",\n \"content\": \"Add filtering and search capabilities\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"6\",\n \"content\": \"Implement priority levels and due dates\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"7\",\n \"content\": \"Add tags and categories\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"8\",\n \"content\": \"Create pretty output formatting\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"9\",\n \"content\": \"Write comprehensive tests\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"10\",\n \"content\": \"Add README with usage examples\",\n \"status\": \"pending\",\n \"priority\": \"low\"\n }\n]","title":"10 todos","metadata":{"todos":[{"id":"1","content":"Design the todo app architecture and data model","status":"in_progress","priority":"high"},{"id":"2","content":"Implement core Todo and TodoList classes","status":"pending","priority":"high"},{"id":"3","content":"Add persistence layer (JSON file storage)","status":"pending","priority":"high"},{"id":"4","content":"Create CLI interface with command handling","status":"pending","priority":"high"},{"id":"5","content":"Add filtering and search capabilities","status":"pending","priority":"medium"},{"id":"6","content":"Implement priority levels and due dates","status":"pending","priority":"medium"},{"id":"7","content":"Add tags and categories","status":"pending","priority":"medium"},{"id":"8","content":"Create pretty output formatting","status":"pending","priority":"medium"},{"id":"9","content":"Write comprehensive tests","status":"pending","priority":"high"},{"id":"10","content":"Add README with usage examples","status":"pending","priority":"low"}],"truncated":false},"time":{"start":1770187087172,"end":1770187087180}}}}
18
+ {"type":"step_finish","timestamp":1770187087202,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f5d51001K0X6r5is6bSpCF","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f17af001hBx2ZnI5s2T7X6","type":"step-finish","reason":"tool-calls","snapshot":"a57194ef5a651225fcd30a8c1693512e5ac64d59","cost":0,"tokens":{"input":2,"output":392,"reasoning":0,"cache":{"read":0,"write":15463}}}}
19
+ {"type":"step_start","timestamp":1770187100723,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f9230001va5bdY1KhjxQG0","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f5d7a001ahslHtCQnw7J1X","type":"step-start","snapshot":"cdd7b1abdd3f43bd9efb116cffba98955ba57a2e"}}
20
+ {"type":"tool_use","timestamp":1770187100734,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f9234001Uu930gEHD2BlmN","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f5d7a001ahslHtCQnw7J1X","type":"tool","callID":"toolu_01EYZZEC8oyTLrULZrUsUEx2","tool":"read","state":{"status":"completed","input":{"filePath":"/home/nathan/src/ralph.rb/specs/README.md"},"output":"<file>\n00001| <!--\n00002| Copyright (c) 2025 Nathan Kidd <nathankidd@hey.com>. All rights reserved.\n00003| SPDX-License-Identifier: Proprietary\n00004| -->\n00005| \n00006| <!--\n00007| HOW TO MAINTAIN THIS FILE\n00008| \n00009| This is the index of all design specifications for Ralph.rb.\n00010| Each row links a spec document to its implementation code and a short purpose summary.\n00011| \n00012| When adding a new spec:\n00013| 1. Create the markdown file in this directory (specs/)\n00014| 2. Add a row to the appropriate table section below\n00015| 3. Link the spec file, the code path it describes, and a brief purpose\n00016| \n00017| Table format:\n00018| | [spec-name.md](./spec-name.md) | [path/to/code](../path/to/code) | Short description |\n00019| \n00020| Use \"—\" in the Code column if the spec has no implementation yet.\n00021| Group specs under heading sections by domain area.\n00022| -->\n00023| \n00024| # Ralph.rb Specifications\n00025| \n00026| Design documentation for Ralph.rb, a Ruby CLI that runs iterative AI development loops.\n00027| \n00028| ## Core\n00029| \n00030| | Spec | Code | Purpose |\n00031| |------|------|---------|\n00032| \n\n(End of file - total 32 lines)\n</file>","title":"specs/README.md","metadata":{"preview":"<!--\n Copyright (c) 2025 Nathan Kidd <nathankidd@hey.com>. All rights reserved.\n SPDX-License-Identifier: Proprietary\n-->\n\n<!--\n HOW TO MAINTAIN THIS FILE\n\n This is the index of all design specifications for Ralph.rb.\n Each row links a spec document to its implementation code and a short purpose summary.\n\n When adding a new spec:\n 1. Create the markdown file in this directory (specs/)\n 2. Add a row to the appropriate table section below\n 3. Link the spec file, the code path it describes, and a brief purpose\n\n Table format:\n | [spec-name.md](./spec-name.md) | [path/to/code](../path/to/code) | Short description |\n\n Use \"—\" in the Code column if the spec has no implementation yet.","truncated":false},"time":{"start":1770187100725,"end":1770187100727}}}}
21
+ {"type":"tool_use","timestamp":1770187100734,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f92310016vUTiBTRHwDM5W","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f5d7a001ahslHtCQnw7J1X","type":"tool","callID":"toolu_013K7yZnRe7rxGp6McUPRv6c","tool":"glob","state":{"status":"completed","input":{"pattern":"**/*.rb"},"output":"No files found","title":"specs","metadata":{"count":0,"truncated":false},"time":{"start":1770187100722,"end":1770187100727}}}}
22
+ {"type":"step_finish","timestamp":1770187100738,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f9238001Bc7W61klSlCgMS","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f5d7a001ahslHtCQnw7J1X","type":"step-finish","reason":"tool-calls","snapshot":"9ff571000c58ef49757c25c596933e95d027b68a","cost":0,"tokens":{"input":5,"output":109,"reasoning":0,"cache":{"read":15463,"write":820}}}}
23
+ {"type":"step_start","timestamp":1770187103298,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f9c3f00174ayQ55QZMnCx1","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f9253001aLp1tmXMfkYWJP","type":"step-start","snapshot":"11b5b96ce076193e7d1845670a5bf54dc1dd710e"}}
24
+ {"type":"tool_use","timestamp":1770187105724,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275f9c7100124pI64UBSSh46E","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f9253001aLp1tmXMfkYWJP","type":"tool","callID":"toolu_01FraqehJ4VqbchH6Yg9xCyB","tool":"todowrite","state":{"status":"completed","input":{"todos":[{"id":"1","content":"Design the todo app architecture and data model","status":"completed","priority":"high"},{"id":"2","content":"Implement core Todo and TodoList classes","status":"in_progress","priority":"high"},{"id":"3","content":"Add persistence layer (JSON file storage)","status":"pending","priority":"high"},{"id":"4","content":"Create CLI interface with command handling","status":"pending","priority":"high"},{"id":"5","content":"Add filtering and search capabilities","status":"pending","priority":"medium"},{"id":"6","content":"Implement priority levels and due dates","status":"pending","priority":"medium"},{"id":"7","content":"Add tags and categories","status":"pending","priority":"medium"},{"id":"8","content":"Create pretty output formatting","status":"pending","priority":"medium"},{"id":"9","content":"Write comprehensive tests","status":"pending","priority":"high"},{"id":"10","content":"Add README with usage examples","status":"pending","priority":"low"}]},"output":"[\n {\n \"id\": \"1\",\n \"content\": \"Design the todo app architecture and data model\",\n \"status\": \"completed\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"2\",\n \"content\": \"Implement core Todo and TodoList classes\",\n \"status\": \"in_progress\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"3\",\n \"content\": \"Add persistence layer (JSON file storage)\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"4\",\n \"content\": \"Create CLI interface with command handling\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"5\",\n \"content\": \"Add filtering and search capabilities\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"6\",\n \"content\": \"Implement priority levels and due dates\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"7\",\n \"content\": \"Add tags and categories\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"8\",\n \"content\": \"Create pretty output formatting\",\n \"status\": \"pending\",\n \"priority\": \"medium\"\n },\n {\n \"id\": \"9\",\n \"content\": \"Write comprehensive tests\",\n \"status\": \"pending\",\n \"priority\": \"high\"\n },\n {\n \"id\": \"10\",\n \"content\": \"Add README with usage examples\",\n \"status\": \"pending\",\n \"priority\": \"low\"\n }\n]","title":"9 todos","metadata":{"todos":[{"id":"1","content":"Design the todo app architecture and data model","status":"completed","priority":"high"},{"id":"2","content":"Implement core Todo and TodoList classes","status":"in_progress","priority":"high"},{"id":"3","content":"Add persistence layer (JSON file storage)","status":"pending","priority":"high"},{"id":"4","content":"Create CLI interface with command handling","status":"pending","priority":"high"},{"id":"5","content":"Add filtering and search capabilities","status":"pending","priority":"medium"},{"id":"6","content":"Implement priority levels and due dates","status":"pending","priority":"medium"},{"id":"7","content":"Add tags and categories","status":"pending","priority":"medium"},{"id":"8","content":"Create pretty output formatting","status":"pending","priority":"medium"},{"id":"9","content":"Write comprehensive tests","status":"pending","priority":"high"},{"id":"10","content":"Add README with usage examples","status":"pending","priority":"low"}],"truncated":false},"time":{"start":1770187105711,"end":1770187105720}}}}
25
+ {"type":"step_finish","timestamp":1770187105737,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275fa5ba001LZse64oqhRnWw6","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275f9253001aLp1tmXMfkYWJP","type":"step-finish","reason":"tool-calls","snapshot":"0012aee2405832cd50eb58cf61f9d6a06d5ec8d7","cost":0,"tokens":{"input":6,"output":354,"reasoning":0,"cache":{"read":16283,"write":575}}}}
26
+ {"type":"step_start","timestamp":1770187107336,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275fac07001OyYeVzom3wXWKJ","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275fa5de001g6TZgrYArTMYDj","type":"step-start","snapshot":"6a96baea47fc3070e5cca8f349aa76d459448b35"}}
27
+ {"type":"tool_use","timestamp":1770187123469,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275fac08001jiLorarTEUEKFX","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275fa5de001g6TZgrYArTMYDj","type":"tool","callID":"toolu_014EWUsB3p8FPzhZJEW2HWPx","tool":"write","state":{"status":"completed","input":{"filePath":"/home/nathan/src/ralph.rb/specs/lib/todo_item.rb","content":"# frozen_string_literal: true\n\n# Copyright (c) 2025 Nathan Kidd <nathankidd@hey.com>. All rights reserved.\n# SPDX-License-Identifier: Proprietary\n\nrequire 'json'\nrequire 'time'\n\n# Represents a single todo item with full feature support\nclass TodoItem\n attr_reader :id, :title, :description, :created_at, :updated_at\n attr_accessor :completed, :priority, :due_date, :tags, :category\n\n PRIORITIES = { low: 1, medium: 2, high: 3, urgent: 4 }.freeze\n\n def initialize(title:, description: nil, priority: :medium, due_date: nil, tags: [], category: nil, id: nil)\n @id = id || generate_id\n @title = title\n @description = description\n @completed = false\n @priority = priority\n @due_date = due_date\n @tags = tags || []\n @category = category\n @created_at = Time.now\n @updated_at = Time.now\n end\n\n def complete!\n if @completed\n @completed = false\n else\n @completed = true\n end\n @updated_at = Time.now\n end\n\n def completed?\n @completed\n end\n\n def overdue?\n if @due_date && !@completed\n Time.parse(@due_date) < Time.now\n else\n false\n end\n end\n\n def due_soon?(hours: 24)\n if @due_date && !@completed\n due_time = Time.parse(@due_date)\n due_time > Time.now && due_time < Time.now + (hours * 3600)\n else\n false\n end\n end\n\n def priority_value\n PRIORITIES[@priority] || PRIORITIES[:medium]\n end\n\n def add_tag(tag)\n unless @tags.include?(tag)\n @tags << tag\n @updated_at = Time.now\n end\n end\n\n def remove_tag(tag)\n if @tags.delete(tag)\n @updated_at = Time.now\n end\n end\n\n def matches_filter?(query: nil, tags: nil, category: nil, completed: nil, priority: nil)\n if query && !matches_query?(query)\n false\n elsif tags && (tags & @tags).empty?\n false\n elsif category && @category != category\n false\n elsif !completed.nil? && @completed != completed\n false\n elsif priority && @priority != priority\n false\n else\n true\n end\n end\n\n def to_h\n {\n id: @id,\n title: @title,\n description: @description,\n completed: @completed,\n priority: @priority,\n due_date: @due_date,\n tags: @tags,\n category: @category,\n created_at: @created_at.iso8601,\n updated_at: @updated_at.iso8601\n }\n end\n\n def self.from_h(hash)\n new(\n id: hash['id'],\n title: hash['title'],\n description: hash['description'],\n priority: hash['priority']&.to_sym || :medium,\n due_date: hash['due_date'],\n tags: hash['tags'] || [],\n category: hash['category']\n ).then do |item|\n item.instance_variable_set(:@completed, hash['completed'] || false)\n item.instance_variable_set(:@created_at, Time.parse(hash['created_at']))\n item.instance_variable_set(:@updated_at, Time.parse(hash['updated_at']))\n item\n end\n end\n\n private\n\n def generate_id\n \"#{Time.now.to_i}-#{rand(10000)}\"\n end\n\n def matches_query?(query)\n query_lower = query.downcase\n if @title.downcase.include?(query_lower)\n true\n elsif @description && @description.downcase.include?(query_lower)\n true\n elsif @tags.any? { |tag| tag.downcase.include?(query_lower) }\n true\n elsif @category && @category.downcase.include?(query_lower)\n true\n else\n false\n end\n end\nend\n"},"output":"","title":"specs/lib/todo_item.rb","metadata":{"diagnostics":{"/home/nathan/src/ralph.rb/specs/lib/todo_item.rb":[]},"filepath":"/home/nathan/src/ralph.rb/specs/lib/todo_item.rb","exists":false,"truncated":false},"time":{"start":1770187120035,"end":1770187123465}}}}
28
+ {"type":"step_finish","timestamp":1770187123482,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275feb0b001BNSMKEYbJq0b91","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275fa5de001g6TZgrYArTMYDj","type":"step-finish","reason":"tool-calls","snapshot":"177366b69da91d17a347fe0ad0f65091e4bea58d","cost":0,"tokens":{"input":5,"output":1164,"reasoning":0,"cache":{"read":16858,"write":798}}}}
29
+ {"type":"step_start","timestamp":1770187125160,"sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","part":{"id":"prt_c275ff1a60017BYnKNMjfD21UA","sessionID":"ses_3d8a0e8abffesEdbo6QsvtRrNS","messageID":"msg_c275feb31001x6mnGGa2J1HWi5","type":"step-start","snapshot":"9edefc0011373084034390a13e04d98067bc123b"}}
30
+ ```
31
+
32
+ ## calculation
33
+
34
+ The context size lives in the `step_finish` events, in the `tokens` field. The total input context for any step is:
35
+
36
+ ```
37
+ context = input + cache.read + cache.write
38
+ ```
39
+
40
+ Walking through your log:
41
+
42
+ | Step | input | cache.read | cache.write | **Total Context** |
43
+ |------|-------|------------|-------------|-------------------|
44
+ | 1 | 2 | 0 | 15,463 | **15,465** |
45
+ | 2 | 5 | 15,463 | 820 | **16,288** |
46
+ | 3 | 6 | 16,283 | 575 | **16,864** |
47
+ | 4 | 5 | 16,858 | 798 | **17,661** |
48
+
49
+ The pattern: each step's `cache.read` ≈ previous step's `cache.read + cache.write`, meaning the previously-seen context gets cached. The `cache.write` is the new tokens added that step (tool results, assistant output). `input` is the tiny uncacheable portion (likely the few tokens at a cache boundary).
50
+
51
+ So to get the current context window usage at any point, sum all three input token fields from the most recent `step_finish`. To track growth over time, chart that sum across steps — you can see context growing ~400-1200 tokens per step here as tool results accumulate.