ollama_agent 0.1.0 → 0.1.1
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 +4 -4
- data/README.md +29 -1
- data/lib/ollama_agent/agent.rb +9 -2
- data/lib/ollama_agent/agent_prompt.rb +15 -0
- data/lib/ollama_agent/cli.rb +159 -1
- data/lib/ollama_agent/diff_path_validator.rb +1 -1
- data/lib/ollama_agent/patch_risk.rb +81 -0
- data/lib/ollama_agent/patch_support.rb +25 -0
- data/lib/ollama_agent/repo_list.rb +4 -1
- data/lib/ollama_agent/ruby_index_tool_support.rb +17 -6
- data/lib/ollama_agent/sandboxed_tools.rb +32 -8
- data/lib/ollama_agent/self_improvement/analyzer.rb +31 -0
- data/lib/ollama_agent/self_improvement/improver.rb +218 -0
- data/lib/ollama_agent/self_improvement/modes.rb +25 -0
- data/lib/ollama_agent/self_improvement.rb +5 -0
- data/lib/ollama_agent/tool_arguments.rb +12 -1
- data/lib/ollama_agent/tools_schema.rb +2 -0
- data/lib/ollama_agent/version.rb +1 -1
- data/lib/ollama_agent.rb +7 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cfef6ceb699a061d21e17bf98b94cd78ae9f25b9a0560aebdc8157bc09f87bb8
|
|
4
|
+
data.tar.gz: b9fbbc92044a421304822f26f25fdb502dbf860686c3e576aa65ec5cf488bae8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f7ca6e5d68e52c682c33503f49629a9709b23238387b8b7cc5dc2622e3f809db08c8e21a43dc80df55aae025895e9485701a03a19f9dcfbc920807bc4f551e92
|
|
7
|
+
data.tar.gz: f4aa8a0a648f42e2b2ad6f4a4adf180d5a7a42b8bb99af279d3c7aa407c96fe0ca7e59234da0ba172a3e1f132c13cad6110ea435ca9b6f8026dc559fd7541f43
|
data/README.md
CHANGED
|
@@ -11,13 +11,23 @@ Ruby gem that runs a **CLI coding agent** against a local [Ollama](https://ollam
|
|
|
11
11
|
- Tool `search_code` – search code with ripgrep or grep.
|
|
12
12
|
- Tool `edit_file` – apply unified diffs safely.
|
|
13
13
|
- CLI built with Thor, entry point `exe/ollama_agent`.
|
|
14
|
+
- **`self_review`** – self-review / improvement with a **`--mode`**:
|
|
15
|
+
- **`analysis`** (default, alias `1`) — read-only tools; report only; no writes.
|
|
16
|
+
- **`interactive`** (alias `2`, `fix`) — full tools on `--root`; you confirm each patch (like `ask`); optional `-y` / `--semi`.
|
|
17
|
+
- **`automated`** (alias `3`, `sandbox`) — temp copy, agent edits, **`bundle exec rspec`** in the sandbox, optional **`--apply`** to merge into your checkout.
|
|
18
|
+
- **`improve`** — same as **`self_review --mode automated`** (you can pass **`--mode automated`** explicitly; other modes belong on **`self_review`**).
|
|
14
19
|
|
|
15
20
|
## Requirements
|
|
16
21
|
|
|
17
|
-
- Ruby ≥ 3.2
|
|
22
|
+
- Ruby ≥ 3.2 (enforced in the gemspec as `required_ruby_version`)
|
|
18
23
|
- **Local:** Ollama running and a capable tool-calling model, **or**
|
|
19
24
|
- **Ollama Cloud:** API key and a cloud-capable model name (see below)
|
|
20
25
|
|
|
26
|
+
### Prerequisites (external tools)
|
|
27
|
+
|
|
28
|
+
- **`patch`** — required for `edit_file` (GNU `patch` on `PATH`). On Windows, use Git Bash, WSL, GnuWin32, or another environment that provides `patch`.
|
|
29
|
+
- **`rg` (ripgrep) or `grep`** — text mode for `search_code` needs at least one of these on `PATH` (ripgrep is preferred when present).
|
|
30
|
+
|
|
21
31
|
## Installation
|
|
22
32
|
|
|
23
33
|
From RubyGems (when published) or from this repository:
|
|
@@ -54,6 +64,23 @@ Interactive REPL:
|
|
|
54
64
|
bundle exec ruby exe/ollama_agent ask --interactive
|
|
55
65
|
```
|
|
56
66
|
|
|
67
|
+
Self-review modes (default project root is the loaded gem tree unless you set `--root` or `OLLAMA_AGENT_ROOT`):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Mode 1 — analysis only (default)
|
|
71
|
+
bundle exec ruby exe/ollama_agent self_review
|
|
72
|
+
bundle exec ruby exe/ollama_agent self_review --mode analysis
|
|
73
|
+
|
|
74
|
+
# Mode 2 — optional fixes in the working tree (confirm each patch, or -y / --semi)
|
|
75
|
+
bundle exec ruby exe/ollama_agent self_review --mode interactive
|
|
76
|
+
|
|
77
|
+
# Mode 3 — sandbox + tests + optional merge back (same as `improve`)
|
|
78
|
+
bundle exec ruby exe/ollama_agent self_review --mode automated
|
|
79
|
+
bundle exec ruby exe/ollama_agent improve --apply
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
For mode 3, `-y` skips all patch prompts; `--no-semi` prompts for every patch when not using `-y`.
|
|
83
|
+
|
|
57
84
|
With a **thinking-capable** model, enable reasoning output:
|
|
58
85
|
|
|
59
86
|
```bash
|
|
@@ -97,6 +124,7 @@ bundle exec ruby exe/ollama_agent ask "Your task"
|
|
|
97
124
|
| `OLLAMA_AGENT_MARKDOWN` | Set to `0` to disable Markdown formatting of assistant replies (plain text only) |
|
|
98
125
|
| `OLLAMA_AGENT_THINKING_MARKDOWN` | Set to `1` to render **thinking** text with Markdown (muted); default is plain dim text inside the Thinking frame |
|
|
99
126
|
| `OLLAMA_AGENT_THINK` | Model **thinking** mode for compatible models: `true` / `false`, or `high` / `medium` / `low` (see ollama-client `think:`). Empty = omit (server default). |
|
|
127
|
+
| `OLLAMA_AGENT_PATCH_RISK_MAX_DIFF_LINES` | Max changed-line count before a diff is treated as "large" for semi-auto patch risk (default **80**) |
|
|
100
128
|
| `OLLAMA_AGENT_INDEX_REBUILD` | Set to `1` to drop the cached Prism Ruby index before the next symbol search in this process |
|
|
101
129
|
| `OLLAMA_AGENT_RUBY_INDEX_MAX_FILES` | Max `.rb` files to parse per index build (default **5000**) |
|
|
102
130
|
| `OLLAMA_AGENT_RUBY_INDEX_MAX_FILE_BYTES` | Skip Ruby files larger than this many bytes (default **512000**) |
|
data/lib/ollama_agent/agent.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative "tool_content_parser"
|
|
|
11
11
|
|
|
12
12
|
module OllamaAgent
|
|
13
13
|
# Runs a tool-calling loop against Ollama: read files, search, apply unified diffs.
|
|
14
|
+
# rubocop:disable Metrics/ClassLength -- Facade for chat loop, tools, and HTTP client wiring
|
|
14
15
|
class Agent
|
|
15
16
|
include SandboxedTools
|
|
16
17
|
|
|
@@ -21,10 +22,13 @@ module OllamaAgent
|
|
|
21
22
|
attr_reader :client, :root
|
|
22
23
|
|
|
23
24
|
# rubocop:disable Metrics/ParameterLists -- CLI and tests pass explicit dependencies
|
|
24
|
-
def initialize(client: nil, model: nil, root: nil, confirm_patches: true, http_timeout: nil, think: nil
|
|
25
|
+
def initialize(client: nil, model: nil, root: nil, confirm_patches: true, http_timeout: nil, think: nil,
|
|
26
|
+
read_only: false, patch_policy: nil)
|
|
25
27
|
@model = model || default_model
|
|
26
28
|
@root = File.expand_path(root || ENV.fetch("OLLAMA_AGENT_ROOT", Dir.pwd))
|
|
27
29
|
@confirm_patches = confirm_patches
|
|
30
|
+
@read_only = read_only
|
|
31
|
+
@patch_policy = patch_policy
|
|
28
32
|
@http_timeout_override = http_timeout
|
|
29
33
|
@think = think
|
|
30
34
|
@client = client || build_default_client
|
|
@@ -82,7 +86,7 @@ module OllamaAgent
|
|
|
82
86
|
def chat_request_args(messages)
|
|
83
87
|
args = {
|
|
84
88
|
messages: messages,
|
|
85
|
-
tools: TOOLS,
|
|
89
|
+
tools: @read_only ? READ_ONLY_TOOLS : TOOLS,
|
|
86
90
|
model: @model,
|
|
87
91
|
options: { temperature: 0.2 }
|
|
88
92
|
}
|
|
@@ -122,6 +126,8 @@ module OllamaAgent
|
|
|
122
126
|
end
|
|
123
127
|
|
|
124
128
|
def system_prompt
|
|
129
|
+
return AgentPrompt.self_review_text if @read_only
|
|
130
|
+
|
|
125
131
|
AgentPrompt.text
|
|
126
132
|
end
|
|
127
133
|
|
|
@@ -143,4 +149,5 @@ module OllamaAgent
|
|
|
143
149
|
msg
|
|
144
150
|
end
|
|
145
151
|
end
|
|
152
|
+
# rubocop:enable Metrics/ClassLength
|
|
146
153
|
end
|
|
@@ -40,5 +40,20 @@ module OllamaAgent
|
|
|
40
40
|
them as the assistant message.
|
|
41
41
|
PROMPT
|
|
42
42
|
end
|
|
43
|
+
|
|
44
|
+
def self.self_review_text
|
|
45
|
+
<<~PROMPT
|
|
46
|
+
You are reviewing the ollama_agent Ruby gem. Tools available: list_files, read_file, search_code only.
|
|
47
|
+
Do not call edit_file and do not output unified diffs—this run is analysis-only.
|
|
48
|
+
|
|
49
|
+
Work only under the project root. Briefly state your plan, then use tools.
|
|
50
|
+
|
|
51
|
+
Large Ruby trees: use search_code with mode "method", "class", "module", or "constant" to locate definitions
|
|
52
|
+
via the Prism index, then read_file with start_line/end_line for only the lines you need.
|
|
53
|
+
|
|
54
|
+
Final reply: strengths, risks, and concrete suggestions with file paths (and line numbers when clear).
|
|
55
|
+
Do not paste JSON tool calls in prose; tools run only via native tool calls from the API.
|
|
56
|
+
PROMPT
|
|
57
|
+
end
|
|
43
58
|
end
|
|
44
59
|
end
|
data/lib/ollama_agent/cli.rb
CHANGED
|
@@ -6,7 +6,12 @@ require_relative "agent"
|
|
|
6
6
|
|
|
7
7
|
module OllamaAgent
|
|
8
8
|
# Thor CLI for single-shot and interactive agent sessions.
|
|
9
|
+
# rubocop:disable Metrics/ClassLength -- Thor commands and shared helpers
|
|
9
10
|
class CLI < Thor
|
|
11
|
+
def self.exit_on_failure?
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
10
15
|
desc "ask [QUERY]", "Run a natural-language task (reads, search, patch)"
|
|
11
16
|
method_option :model, type: :string, desc: "Ollama model (default: OLLAMA_AGENT_MODEL or ollama-client default)"
|
|
12
17
|
method_option :interactive, type: :boolean, aliases: "-i", desc: "Interactive REPL"
|
|
@@ -27,12 +32,164 @@ module OllamaAgent
|
|
|
27
32
|
end
|
|
28
33
|
end
|
|
29
34
|
|
|
35
|
+
desc "self_review", "Self-review / improvement: --mode analysis | interactive | automated (see help)"
|
|
36
|
+
method_option :mode, type: :string, default: "analysis",
|
|
37
|
+
desc: "analysis (1)=read-only report; interactive (2)=confirm patches in tree; " \
|
|
38
|
+
"automated (3)=sandbox+RSpec+optional --apply"
|
|
39
|
+
long_desc <<~HELP
|
|
40
|
+
Modes:
|
|
41
|
+
analysis Read-only tools; prints a report; no writes (default).
|
|
42
|
+
interactive Full tools on --root; you confirm each patch (like `ask`); optional -y / --semi.
|
|
43
|
+
automated Temp sandbox, agent edits, `bundle exec rspec`; optional --apply to merge back.
|
|
44
|
+
|
|
45
|
+
Aliases: 1/2/3, readonly, fix, confirm, sandbox, full.
|
|
46
|
+
HELP
|
|
47
|
+
method_option :model, type: :string, desc: "Ollama model (default: OLLAMA_AGENT_MODEL or ollama-client default)"
|
|
48
|
+
method_option :root, type: :string, desc: "Project root (default: OLLAMA_AGENT_ROOT or gem root)"
|
|
49
|
+
method_option :timeout, type: :numeric, aliases: "-t", desc: "HTTP timeout seconds (default 120)"
|
|
50
|
+
method_option :think, type: :string, desc: "Thinking mode: true|false|high|medium|low (see OLLAMA_AGENT_THINK)"
|
|
51
|
+
method_option :yes, type: :boolean, aliases: "-y",
|
|
52
|
+
desc: "interactive/automated: apply patches without confirmation"
|
|
53
|
+
method_option :semi, type: :boolean, default: true,
|
|
54
|
+
desc: "interactive/automated: auto-approve obvious patches; prompt for risky (default: true)"
|
|
55
|
+
method_option :apply, type: :boolean, default: false,
|
|
56
|
+
desc: "automated only: after green tests, copy changed files from sandbox into --root"
|
|
57
|
+
def self_review
|
|
58
|
+
dispatch_self_review_mode(SelfImprovement::Modes.normalize(options[:mode]))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
desc "improve", "Shortcut for: self_review --mode automated"
|
|
62
|
+
method_option :mode, type: :string, default: "automated",
|
|
63
|
+
desc: "Optional; must be automated (or 3, sandbox, full). Other modes: use self_review --mode"
|
|
64
|
+
method_option :model, type: :string, desc: "Ollama model (default: OLLAMA_AGENT_MODEL or ollama-client default)"
|
|
65
|
+
method_option :root, type: :string, desc: "Source tree to copy and test (default: OLLAMA_AGENT_ROOT or gem root)"
|
|
66
|
+
method_option :timeout, type: :numeric, aliases: "-t", desc: "HTTP timeout seconds (default 120)"
|
|
67
|
+
method_option :think, type: :string, desc: "Thinking mode: true|false|high|medium|low (see OLLAMA_AGENT_THINK)"
|
|
68
|
+
method_option :yes, type: :boolean, aliases: "-y", desc: "Apply all patches without confirmation"
|
|
69
|
+
method_option :semi, type: :boolean, default: true,
|
|
70
|
+
desc: "Without -y: auto-approve obvious patches; prompt for risky (default: true)"
|
|
71
|
+
method_option :apply, type: :boolean, default: false,
|
|
72
|
+
desc: "After green tests, copy changed files from sandbox into --root"
|
|
73
|
+
def improve
|
|
74
|
+
ensure_improve_mode_only_automated!
|
|
75
|
+
dispatch_self_review_mode("automated")
|
|
76
|
+
end
|
|
77
|
+
|
|
30
78
|
private
|
|
31
79
|
|
|
80
|
+
def ensure_improve_mode_only_automated!
|
|
81
|
+
m = SelfImprovement::Modes.normalize(options[:mode])
|
|
82
|
+
return if m == "automated"
|
|
83
|
+
|
|
84
|
+
warn Console.error_line(improve_mode_error_message(m))
|
|
85
|
+
exit 1
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def improve_mode_error_message(normalized_mode)
|
|
89
|
+
return invalid_improve_mode_message unless SelfImprovement::Modes.valid?(normalized_mode)
|
|
90
|
+
|
|
91
|
+
"Command \"improve\" only runs automated (sandbox) mode. For other modes use: " \
|
|
92
|
+
"ollama_agent self_review --mode analysis | interactive"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def invalid_improve_mode_message
|
|
96
|
+
"Invalid --mode for improve: use automated (or 3, sandbox, full). Got: #{options[:mode].inspect}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def dispatch_self_review_mode(mode)
|
|
100
|
+
validate_self_review_mode!(mode)
|
|
101
|
+
send(:"run_mode_#{mode}")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def validate_self_review_mode!(mode)
|
|
105
|
+
return if SelfImprovement::Modes.valid?(mode)
|
|
106
|
+
|
|
107
|
+
warn Console.error_line(
|
|
108
|
+
"Invalid --mode: use analysis, interactive, or automated (or 1, 2, 3). Got: #{options[:mode].inspect}"
|
|
109
|
+
)
|
|
110
|
+
exit 1
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def run_mode_analysis
|
|
114
|
+
agent = Agent.new(
|
|
115
|
+
model: options[:model],
|
|
116
|
+
root: resolved_root_for_self_review,
|
|
117
|
+
read_only: true,
|
|
118
|
+
confirm_patches: false,
|
|
119
|
+
http_timeout: options[:timeout],
|
|
120
|
+
think: options[:think]
|
|
121
|
+
)
|
|
122
|
+
SelfImprovement::Analyzer.new(agent).run
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def run_mode_interactive
|
|
126
|
+
agent = Agent.new(**interactive_agent_keywords)
|
|
127
|
+
SelfImprovement::Analyzer.new(agent).run(SelfImprovement::Analyzer::INTERACTIVE_PROMPT)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def interactive_agent_keywords
|
|
131
|
+
semi = options[:semi] != false
|
|
132
|
+
{
|
|
133
|
+
model: options[:model],
|
|
134
|
+
root: resolved_root_for_self_review,
|
|
135
|
+
read_only: false,
|
|
136
|
+
confirm_patches: !options[:yes],
|
|
137
|
+
patch_policy: semi ? PatchRisk.method(:assess).to_proc : nil,
|
|
138
|
+
http_timeout: options[:timeout],
|
|
139
|
+
think: options[:think]
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def run_mode_automated
|
|
144
|
+
result = SelfImprovement::Improver.new.run(**improve_run_options)
|
|
145
|
+
report_improve_result(result)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def resolved_root_for_self_review
|
|
149
|
+
raw = options[:root] || ENV.fetch("OLLAMA_AGENT_ROOT", nil)
|
|
150
|
+
base = raw.to_s.strip.empty? ? OllamaAgent.gem_root : raw
|
|
151
|
+
File.expand_path(base)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def improve_run_options
|
|
155
|
+
{
|
|
156
|
+
model: options[:model],
|
|
157
|
+
root: options[:root] || ENV.fetch("OLLAMA_AGENT_ROOT", nil),
|
|
158
|
+
yes: options[:yes],
|
|
159
|
+
semi: options[:semi] != false,
|
|
160
|
+
apply: options[:apply],
|
|
161
|
+
http_timeout: options[:timeout],
|
|
162
|
+
think: options[:think]
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def report_improve_result(result)
|
|
167
|
+
unless result[:success]
|
|
168
|
+
warn Console.error_line("Tests failed in sandbox.")
|
|
169
|
+
puts result[:test_output]
|
|
170
|
+
exit 1
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
root = result[:source_root]
|
|
174
|
+
puts "ollama_agent: tests passed in sandbox (#{root})"
|
|
175
|
+
copied = result[:copied_to_source]
|
|
176
|
+
report_improve_copied_files(copied, root)
|
|
177
|
+
puts "ollama_agent: no changed files to copy from sandbox" if options[:apply] && copied.empty?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def report_improve_copied_files(copied, root)
|
|
181
|
+
return if copied.empty?
|
|
182
|
+
|
|
183
|
+
puts "ollama_agent: copied #{copied.size} file(s) to #{root}"
|
|
184
|
+
copied.sort.each { |rel| puts " #{rel}" }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Build an Agent for the `ask` command.
|
|
188
|
+
# Use the same root resolution as other commands, falling back to the gem root.
|
|
32
189
|
def build_agent
|
|
33
190
|
Agent.new(
|
|
34
191
|
model: options[:model],
|
|
35
|
-
root:
|
|
192
|
+
root: resolved_root_for_self_review,
|
|
36
193
|
confirm_patches: !options[:yes],
|
|
37
194
|
http_timeout: options[:timeout],
|
|
38
195
|
think: options[:think]
|
|
@@ -70,4 +227,5 @@ module OllamaAgent
|
|
|
70
227
|
end
|
|
71
228
|
end
|
|
72
229
|
end
|
|
230
|
+
# rubocop:enable Metrics/ClassLength
|
|
73
231
|
end
|
|
@@ -77,7 +77,7 @@ module OllamaAgent
|
|
|
77
77
|
|
|
78
78
|
<<~MSG.strip
|
|
79
79
|
A unified diff must list --- a/<path>, then +++ b/<path>, then @@ ... @@ before any changed lines.
|
|
80
|
-
|
|
80
|
+
Put a +++ b/<file> line before the first @@ hunk (e.g. +++ b/README.md); do not place @@ immediately after --- alone.
|
|
81
81
|
MSG
|
|
82
82
|
end
|
|
83
83
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OllamaAgent
|
|
4
|
+
# Classifies a proposed unified diff for semi-automatic patch approval (obvious vs risky).
|
|
5
|
+
module PatchRisk
|
|
6
|
+
FORBIDDEN_PATTERNS = [
|
|
7
|
+
/\beval\s*\(/,
|
|
8
|
+
/`rm\s+-rf/,
|
|
9
|
+
/system\s*\(\s*["']sudo/,
|
|
10
|
+
/\bFile\.delete\b/,
|
|
11
|
+
/\bKernel\.exec\b/
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
LARGE_DIFF_LINES = 80
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def large_diff_line_limit
|
|
19
|
+
v = ENV.fetch("OLLAMA_AGENT_PATCH_RISK_MAX_DIFF_LINES", nil)
|
|
20
|
+
return LARGE_DIFF_LINES if v.nil? || v.to_s.strip.empty?
|
|
21
|
+
|
|
22
|
+
Integer(v)
|
|
23
|
+
rescue ArgumentError, TypeError
|
|
24
|
+
LARGE_DIFF_LINES
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def forbidden?(diff)
|
|
28
|
+
FORBIDDEN_PATTERNS.any? { |pattern| diff.match?(pattern) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns :auto_approve (no prompt) or :require_confirmation (prompt when confirm_patches is on).
|
|
32
|
+
def assess(path, diff)
|
|
33
|
+
relative = path.to_s.tr("\\", "/")
|
|
34
|
+
|
|
35
|
+
return :require_confirmation if risky?(relative, diff)
|
|
36
|
+
|
|
37
|
+
return :auto_approve if obvious_path?(relative)
|
|
38
|
+
return :auto_approve if safe_spec_change?(relative, diff)
|
|
39
|
+
|
|
40
|
+
:require_confirmation
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def risky?(relative, diff)
|
|
44
|
+
forbidden?(diff) ||
|
|
45
|
+
critical_path?(relative) ||
|
|
46
|
+
large_diff?(diff) ||
|
|
47
|
+
critical_lib_file?(relative)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def large_diff?(diff, limit: nil)
|
|
51
|
+
max = limit.nil? ? large_diff_line_limit : limit
|
|
52
|
+
diff.scan(/^[-+][^-+]/).size > max
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def obvious_path?(relative)
|
|
56
|
+
relative.end_with?(".md") || relative.start_with?("docs/")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def safe_spec_change?(relative, diff)
|
|
60
|
+
relative.start_with?("spec/") && !large_diff?(diff, limit: 40)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def critical_lib_file?(relative)
|
|
64
|
+
return false unless relative.start_with?("lib/")
|
|
65
|
+
|
|
66
|
+
relative.include?("sandboxed_tools") ||
|
|
67
|
+
relative.include?("patch_support") ||
|
|
68
|
+
relative.end_with?("ollama_agent/agent.rb") ||
|
|
69
|
+
relative.end_with?("ollama_agent/tools_schema.rb")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def critical_path?(relative)
|
|
73
|
+
return true if relative.match?(/\A(Gemfile|Gemfile\.lock)\z/)
|
|
74
|
+
return true if relative.end_with?(".gemspec")
|
|
75
|
+
return true if relative == "lib/ollama_agent/version.rb"
|
|
76
|
+
return true if relative.start_with?("exe/")
|
|
77
|
+
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -8,6 +8,8 @@ module OllamaAgent
|
|
|
8
8
|
private
|
|
9
9
|
|
|
10
10
|
def patch_dry_run(diff)
|
|
11
|
+
return patch_missing_tool_message unless patch_available?
|
|
12
|
+
|
|
11
13
|
output, status = Open3.capture2e(
|
|
12
14
|
"patch", "-p1", "-f", "-d", @root, "--dry-run",
|
|
13
15
|
stdin_data: diff
|
|
@@ -65,6 +67,8 @@ module OllamaAgent
|
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
def apply_patch(diff)
|
|
70
|
+
return patch_missing_tool_message unless patch_available?
|
|
71
|
+
|
|
68
72
|
output, status = Open3.capture2e(
|
|
69
73
|
"patch", "-p1", "-f", "-d", @root,
|
|
70
74
|
stdin_data: diff
|
|
@@ -74,5 +78,26 @@ module OllamaAgent
|
|
|
74
78
|
|
|
75
79
|
patch_failure_message(output, dry_run: false)
|
|
76
80
|
end
|
|
81
|
+
|
|
82
|
+
def patch_available?
|
|
83
|
+
return @patch_available if defined?(@patch_available)
|
|
84
|
+
|
|
85
|
+
@patch_available = patch_binary_usable?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def patch_binary_usable?
|
|
89
|
+
_, status = Open3.capture2e("patch", "--version")
|
|
90
|
+
return true if status.success?
|
|
91
|
+
|
|
92
|
+
_, which_status = Open3.capture2e("which", "patch")
|
|
93
|
+
which_status.success?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def patch_missing_tool_message
|
|
97
|
+
<<~MSG.strip
|
|
98
|
+
Error: ollama_agent: the `patch` program was not found or is not usable on PATH. edit_file requires GNU patch.
|
|
99
|
+
Install it (e.g. apt install patch, brew install gpatch), or on Windows use Git Bash, WSL, or GnuWin32.
|
|
100
|
+
MSG
|
|
101
|
+
end
|
|
77
102
|
end
|
|
78
103
|
end
|
|
@@ -21,7 +21,10 @@ module OllamaAgent
|
|
|
21
21
|
paths = collect_relative_paths(base, cap)
|
|
22
22
|
return "(no files listed)" if paths.empty?
|
|
23
23
|
|
|
24
|
-
paths.sort.join("\n")
|
|
24
|
+
body = paths.sort.join("\n")
|
|
25
|
+
return body unless paths.size >= cap
|
|
26
|
+
|
|
27
|
+
"#{body}\n(list truncated at #{cap} entries; pass max_entries or narrow directory)"
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
def collect_relative_paths(base, cap)
|
|
@@ -39,14 +39,25 @@ module OllamaAgent
|
|
|
39
39
|
|
|
40
40
|
# Rebuild only when OLLAMA_AGENT_INDEX_REBUILD changes (avoids rebuilding on every call while it stays "1").
|
|
41
41
|
def ruby_index
|
|
42
|
-
|
|
43
|
-
@
|
|
44
|
-
|
|
42
|
+
@ruby_index_mutex ||= Mutex.new
|
|
43
|
+
@ruby_index_mutex.synchronize { synchronized_ruby_index }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def synchronized_ruby_index
|
|
47
|
+
invalidate_ruby_index_if_fingerprint_changed
|
|
45
48
|
return @ruby_index if @ruby_index
|
|
46
49
|
|
|
47
|
-
@ruby_index = RubyIndex.build(root: @root)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
@ruby_index = RubyIndex.build(root: @root).tap do |idx|
|
|
51
|
+
warn "ollama_agent: #{idx.summary_line}" if ENV["OLLAMA_AGENT_DEBUG"] == "1"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def invalidate_ruby_index_if_fingerprint_changed
|
|
56
|
+
fp = ENV.fetch("OLLAMA_AGENT_INDEX_REBUILD", "")
|
|
57
|
+
return if fp == @ruby_index_cache_fingerprint
|
|
58
|
+
|
|
59
|
+
@ruby_index = nil
|
|
60
|
+
@ruby_index_cache_fingerprint = fp
|
|
50
61
|
end
|
|
51
62
|
end
|
|
52
63
|
end
|
|
@@ -5,6 +5,7 @@ require "pathname"
|
|
|
5
5
|
|
|
6
6
|
require_relative "console"
|
|
7
7
|
require_relative "diff_path_validator"
|
|
8
|
+
require_relative "patch_risk"
|
|
8
9
|
require_relative "patch_support"
|
|
9
10
|
require_relative "repo_list"
|
|
10
11
|
require_relative "ruby_index_tool_support"
|
|
@@ -48,10 +49,10 @@ module OllamaAgent
|
|
|
48
49
|
pattern = tool_arg(args, "pattern").to_s
|
|
49
50
|
mode = (tool_arg(args, "mode") || "text").to_s.downcase
|
|
50
51
|
|
|
51
|
-
return search_code_ruby(pattern, mode) if ruby_search_mode?(mode)
|
|
52
|
-
|
|
53
52
|
return missing_tool_argument("search_code", "pattern") if blank_tool_value?(pattern)
|
|
54
53
|
|
|
54
|
+
return search_code_ruby(pattern, mode) if ruby_search_mode?(mode)
|
|
55
|
+
|
|
55
56
|
search_code(pattern, tool_arg(args, "directory") || ".")
|
|
56
57
|
end
|
|
57
58
|
|
|
@@ -84,7 +85,8 @@ module OllamaAgent
|
|
|
84
85
|
|
|
85
86
|
def read_file_too_large(abs)
|
|
86
87
|
n = max_read_file_bytes
|
|
87
|
-
"Error reading file: file
|
|
88
|
+
"Error reading file: ollama_agent: file too large for full read (max #{n} bytes); use read_file with " \
|
|
89
|
+
"start_line and end_line, or raise OLLAMA_AGENT_MAX_READ_FILE_BYTES. Path: #{abs}"
|
|
88
90
|
end
|
|
89
91
|
|
|
90
92
|
def max_read_file_bytes
|
|
@@ -137,38 +139,60 @@ module OllamaAgent
|
|
|
137
139
|
dir = directory.to_s.empty? ? "." : directory
|
|
138
140
|
return disallowed_path_message(dir) unless path_allowed?(dir)
|
|
139
141
|
|
|
140
|
-
|
|
142
|
+
return search_code_no_backends_message unless rg_available? || grep_available?
|
|
143
|
+
|
|
144
|
+
return search_with_ripgrep(pattern, dir) if rg_available?
|
|
145
|
+
|
|
146
|
+
search_with_grep!(pattern, dir)
|
|
141
147
|
end
|
|
142
148
|
|
|
143
149
|
def search_with_ripgrep(pattern, directory)
|
|
144
|
-
return nil unless rg_available?
|
|
145
|
-
|
|
146
150
|
stdout, = Open3.capture2("rg", "-n", "--", pattern, resolve_path(directory))
|
|
147
151
|
stdout.to_s
|
|
148
152
|
end
|
|
149
153
|
|
|
150
|
-
def search_with_grep(pattern, directory)
|
|
154
|
+
def search_with_grep!(pattern, directory)
|
|
151
155
|
stdout, = Open3.capture2("grep", "-rn", "--", pattern, resolve_path(directory))
|
|
152
156
|
stdout.to_s
|
|
153
157
|
end
|
|
154
158
|
|
|
159
|
+
def search_code_no_backends_message
|
|
160
|
+
<<~MSG.strip
|
|
161
|
+
Error: ollama_agent: no text search backend available. Install ripgrep (`rg`) or GNU grep on PATH.
|
|
162
|
+
MSG
|
|
163
|
+
end
|
|
164
|
+
|
|
155
165
|
def rg_available?
|
|
156
166
|
system("which", "rg", out: File::NULL, err: File::NULL)
|
|
157
167
|
end
|
|
158
168
|
|
|
169
|
+
def grep_available?
|
|
170
|
+
system("which", "grep", out: File::NULL, err: File::NULL)
|
|
171
|
+
end
|
|
172
|
+
|
|
159
173
|
def edit_file(path, diff)
|
|
160
174
|
return disallowed_path_message(path) unless path_allowed?(path)
|
|
175
|
+
return "edit_file is disabled in read-only mode." if @read_only
|
|
161
176
|
|
|
162
177
|
diff = DiffPathValidator.normalize_diff(diff)
|
|
163
178
|
|
|
164
179
|
validation = validate_edit_diff(path, diff)
|
|
165
180
|
return validation if validation
|
|
166
181
|
|
|
167
|
-
return "
|
|
182
|
+
return "Rejected: diff matches a forbidden pattern (unsafe)." if PatchRisk.forbidden?(diff)
|
|
183
|
+
|
|
184
|
+
return "Cancelled by user" if patch_confirmation_needed?(path, diff) && !user_confirms_patch?(path, diff)
|
|
168
185
|
|
|
169
186
|
apply_patch(diff)
|
|
170
187
|
end
|
|
171
188
|
|
|
189
|
+
def patch_confirmation_needed?(path, diff)
|
|
190
|
+
return false unless @confirm_patches
|
|
191
|
+
return true unless @patch_policy
|
|
192
|
+
|
|
193
|
+
@patch_policy.call(path, diff) == :require_confirmation
|
|
194
|
+
end
|
|
195
|
+
|
|
172
196
|
def validate_edit_diff(path, diff)
|
|
173
197
|
mismatch = DiffPathValidator.call(diff, @root, path)
|
|
174
198
|
return log_tool_message(mismatch) if mismatch
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../agent"
|
|
4
|
+
|
|
5
|
+
module OllamaAgent
|
|
6
|
+
module SelfImprovement
|
|
7
|
+
# Runs a read-only agent pass to review the gem (no edits).
|
|
8
|
+
class Analyzer
|
|
9
|
+
DEFAULT_PROMPT = <<~PROMPT
|
|
10
|
+
Perform a self-review of this gem: architecture, tests, clarity, and risks.
|
|
11
|
+
Use tools to inspect real files; finish with prioritized, actionable recommendations.
|
|
12
|
+
PROMPT
|
|
13
|
+
|
|
14
|
+
# Mode 2 (interactive): full tool loop on the real tree; user confirms each patch like `ask`.
|
|
15
|
+
INTERACTIVE_PROMPT = <<~PROMPT
|
|
16
|
+
You are improving the ollama_agent gem in this working tree (not a sandbox).
|
|
17
|
+
Use list_files, search_code, and read_file, then apply small unified diffs with edit_file.
|
|
18
|
+
Keep changes reviewable; prefer tests, docs, and clarity fixes.
|
|
19
|
+
When finished, summarize what you changed or suggested next.
|
|
20
|
+
PROMPT
|
|
21
|
+
|
|
22
|
+
def initialize(agent)
|
|
23
|
+
@agent = agent
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run(prompt = DEFAULT_PROMPT)
|
|
27
|
+
@agent.run(prompt)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "pathname"
|
|
6
|
+
|
|
7
|
+
require_relative "../agent"
|
|
8
|
+
require_relative "../patch_risk"
|
|
9
|
+
|
|
10
|
+
module OllamaAgent
|
|
11
|
+
module SelfImprovement
|
|
12
|
+
# Copies the project into a temp directory, runs the agent with optional semi-auto patch policy,
|
|
13
|
+
# runs the test suite in the sandbox, and optionally merges changed files back to the source tree.
|
|
14
|
+
# rubocop:disable Metrics/ClassLength -- orchestration + restore + merge helpers
|
|
15
|
+
class Improver
|
|
16
|
+
SANDBOX_EXCLUDE = %w[.git vendor coverage tmp .bundle .cursor node_modules .rspec_status].freeze
|
|
17
|
+
|
|
18
|
+
# Basenames never merged back from the sandbox (test runs create these; they are gitignored).
|
|
19
|
+
MERGE_SKIP_BASENAMES = %w[.rspec_status].freeze
|
|
20
|
+
|
|
21
|
+
FIX_PROMPT = <<~PROMPT
|
|
22
|
+
You are improving the ollama_agent Ruby gem in this temporary sandbox copy.
|
|
23
|
+
Use list_files, search_code, and read_file to understand the code, then edit_file with valid unified diffs.
|
|
24
|
+
Prefer small, reviewable changes: fixes, tests, docs, and clarity.
|
|
25
|
+
Minimal diffs only: fewest lines per edit_file, exact @@ counts—no whole-method or mega-hunks.
|
|
26
|
+
Do not delete Gemfile, Gemfile.lock, the gemspec, or exe/; the improve run restores those from the source
|
|
27
|
+
tree before tests, but deleting them breaks the session.
|
|
28
|
+
Do not add or rely on .rspec_status or other ignored test-artifact files; RSpec may create them during the test step.
|
|
29
|
+
|
|
30
|
+
When finished, summarize what you changed in plain language.
|
|
31
|
+
PROMPT
|
|
32
|
+
|
|
33
|
+
# rubocop:disable Metrics/ParameterLists -- mirrors CLI; keeps call sites explicit
|
|
34
|
+
# rubocop:disable Metrics/MethodLength
|
|
35
|
+
def run(model: nil, root: nil, yes: false, semi: true, apply: false, http_timeout: nil, think: nil, client: nil)
|
|
36
|
+
source_root = resolve_source_root(root)
|
|
37
|
+
sandbox_root = Dir.mktmpdir("ollama_agent_improve_")
|
|
38
|
+
policy = semi ? PatchRisk.method(:assess).to_proc : nil
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
copy_project_into_sandbox(source_root, sandbox_root)
|
|
42
|
+
run_agent_session(
|
|
43
|
+
sandbox_root,
|
|
44
|
+
client: client,
|
|
45
|
+
model: model,
|
|
46
|
+
confirm_patches: !yes,
|
|
47
|
+
patch_policy: policy,
|
|
48
|
+
http_timeout: http_timeout,
|
|
49
|
+
think: think
|
|
50
|
+
)
|
|
51
|
+
restore_build_essentials_from_source(source_root, sandbox_root)
|
|
52
|
+
missing = missing_gemfile_failure(source_root, sandbox_root)
|
|
53
|
+
return build_run_result(missing, [], source_root) if missing
|
|
54
|
+
|
|
55
|
+
test_result = run_test_suite(sandbox_root)
|
|
56
|
+
copied = copy_back_if_requested(test_result, apply, sandbox_root, source_root)
|
|
57
|
+
build_run_result(test_result, copied, source_root)
|
|
58
|
+
ensure
|
|
59
|
+
FileUtils.remove_entry(sandbox_root) if sandbox_root && Dir.exist?(sandbox_root)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
# rubocop:enable Metrics/MethodLength
|
|
63
|
+
# rubocop:enable Metrics/ParameterLists
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def run_agent_session(sandbox_root, **)
|
|
68
|
+
agent = Agent.new(root: sandbox_root, **)
|
|
69
|
+
agent.run(FIX_PROMPT)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def copy_back_if_requested(test_result, apply, sandbox_root, source_root)
|
|
73
|
+
return [] unless test_result[:success] && apply
|
|
74
|
+
|
|
75
|
+
merge_sandbox_into_source(sandbox_root, source_root)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def build_run_result(test_result, copied, source_root)
|
|
79
|
+
{
|
|
80
|
+
success: test_result[:success],
|
|
81
|
+
test_output: test_result[:output],
|
|
82
|
+
copied_to_source: copied,
|
|
83
|
+
source_root: source_root
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def copy_project_into_sandbox(source, dest)
|
|
88
|
+
Dir.children(source).each do |entry|
|
|
89
|
+
next if SANDBOX_EXCLUDE.include?(entry)
|
|
90
|
+
|
|
91
|
+
FileUtils.cp_r(File.join(source, entry), File.join(dest, entry))
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def resolve_source_root(root)
|
|
96
|
+
start_dir = normalize_improve_root(root)
|
|
97
|
+
nearest = nearest_directory_with_gemfile(start_dir)
|
|
98
|
+
nearest || start_dir
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def normalize_improve_root(root)
|
|
102
|
+
return default_improve_source_root if root.nil? || root.to_s.strip.empty?
|
|
103
|
+
|
|
104
|
+
File.expand_path(root)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Installed gems omit Gemfile (see gemspec); gem_root may not contain one. Prefer cwd / env from CLI.
|
|
108
|
+
def default_improve_source_root
|
|
109
|
+
from_cwd = nearest_directory_with_gemfile(Dir.pwd)
|
|
110
|
+
return from_cwd if from_cwd
|
|
111
|
+
|
|
112
|
+
from_gem = nearest_directory_with_gemfile(OllamaAgent.gem_root)
|
|
113
|
+
return from_gem if from_gem
|
|
114
|
+
|
|
115
|
+
OllamaAgent.gem_root
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def nearest_directory_with_gemfile(start_dir)
|
|
119
|
+
dir = File.expand_path(start_dir)
|
|
120
|
+
loop do
|
|
121
|
+
return dir if File.file?(File.join(dir, "Gemfile"))
|
|
122
|
+
|
|
123
|
+
parent = File.dirname(dir)
|
|
124
|
+
return nil if parent == dir
|
|
125
|
+
|
|
126
|
+
dir = parent
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def missing_gemfile_failure(source_root, sandbox_root)
|
|
131
|
+
return nil if File.file?(File.join(sandbox_root, "Gemfile"))
|
|
132
|
+
|
|
133
|
+
msg = <<~MSG
|
|
134
|
+
Cannot run tests: Gemfile is missing in the sandbox after restore from #{source_root}.
|
|
135
|
+
Run `improve` from your project checkout, set OLLAMA_AGENT_ROOT, or pass --root to a tree that contains a Gemfile.
|
|
136
|
+
(The packaged gem does not ship a Gemfile; cwd is used when gem_root has none.)
|
|
137
|
+
MSG
|
|
138
|
+
{ success: false, output: msg.strip }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The model may delete or corrupt Gemfile / lock / gemspec during edit_file; bundle needs them in the sandbox.
|
|
142
|
+
def restore_build_essentials_from_source(source, sandbox)
|
|
143
|
+
%w[Gemfile Gemfile.lock Rakefile .ruby-version].each do |name|
|
|
144
|
+
src = File.join(source, name)
|
|
145
|
+
next unless File.file?(src)
|
|
146
|
+
|
|
147
|
+
FileUtils.cp(src, File.join(sandbox, name))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
Dir.glob(File.join(source, "*.gemspec")).each do |src|
|
|
151
|
+
FileUtils.cp(src, File.join(sandbox, File.basename(src)))
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def run_test_suite(dir)
|
|
156
|
+
output, status = bundle_exec(dir, "rspec", "spec/")
|
|
157
|
+
return { success: true, output: output } if status.success?
|
|
158
|
+
|
|
159
|
+
install_out, install_status = bundle_install(dir)
|
|
160
|
+
combined = "#{install_out}\n#{output}"
|
|
161
|
+
return { success: false, output: combined } unless install_status.success?
|
|
162
|
+
|
|
163
|
+
output2, status2 = bundle_exec(dir, "rspec", "spec/")
|
|
164
|
+
{ success: status2.success?, output: "#{combined}\n#{output2}" }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def bundle_env(dir)
|
|
168
|
+
{ "BUNDLE_GEMFILE" => File.expand_path(File.join(dir, "Gemfile")) }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def bundle_exec(dir, *)
|
|
172
|
+
Open3.capture2e(bundle_env(dir), "bundle", "exec", *, chdir: dir)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def bundle_install(dir)
|
|
176
|
+
Open3.capture2e(bundle_env(dir), "bundle", "install", chdir: dir)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# rubocop:disable Metrics/MethodLength -- straight-line file copy loop
|
|
180
|
+
def merge_sandbox_into_source(sandbox, source)
|
|
181
|
+
paths = []
|
|
182
|
+
each_relative_file(sandbox) do |rel|
|
|
183
|
+
next if MERGE_SKIP_BASENAMES.include?(File.basename(rel))
|
|
184
|
+
|
|
185
|
+
src = File.join(sandbox, rel)
|
|
186
|
+
dst = File.join(source, rel)
|
|
187
|
+
next unless File.file?(src)
|
|
188
|
+
next unless file_differs?(src, dst)
|
|
189
|
+
|
|
190
|
+
FileUtils.mkdir_p(File.dirname(dst))
|
|
191
|
+
FileUtils.cp(src, dst)
|
|
192
|
+
paths << rel
|
|
193
|
+
end
|
|
194
|
+
paths
|
|
195
|
+
end
|
|
196
|
+
# rubocop:enable Metrics/MethodLength
|
|
197
|
+
|
|
198
|
+
def each_relative_file(base)
|
|
199
|
+
base_path = Pathname.new(base)
|
|
200
|
+
Dir.glob(File.join(base, "**", "*"), File::FNM_DOTMATCH).each do |abs|
|
|
201
|
+
next if File.directory?(abs)
|
|
202
|
+
next if abs.include?("#{File::SEPARATOR}vendor#{File::SEPARATOR}")
|
|
203
|
+
next if abs.include?("#{File::SEPARATOR}.git#{File::SEPARATOR}")
|
|
204
|
+
|
|
205
|
+
rel = Pathname(abs).relative_path_from(base_path).to_s
|
|
206
|
+
yield rel
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def file_differs?(sandbox_file, target_file)
|
|
211
|
+
return true unless File.file?(target_file)
|
|
212
|
+
|
|
213
|
+
File.binread(sandbox_file) != File.binread(target_file)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
# rubocop:enable Metrics/ClassLength
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OllamaAgent
|
|
4
|
+
module SelfImprovement
|
|
5
|
+
# CLI modes for self_review / improve (analysis-only, interactive fixes, automated sandbox).
|
|
6
|
+
module Modes
|
|
7
|
+
VALID = %w[analysis interactive automated].freeze
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def normalize(mode)
|
|
12
|
+
case mode.to_s.strip.downcase
|
|
13
|
+
when "", "analysis", "1", "readonly", "read-only" then "analysis"
|
|
14
|
+
when "interactive", "2", "fix", "confirm" then "interactive"
|
|
15
|
+
when "automated", "3", "sandbox", "full" then "automated"
|
|
16
|
+
else mode.to_s.strip.downcase
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def valid?(mode)
|
|
21
|
+
VALID.include?(mode)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -12,7 +12,18 @@ module OllamaAgent
|
|
|
12
12
|
inner = h["parameters"] || h[:parameters]
|
|
13
13
|
return h unless inner.is_a?(Hash)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
outer = h.except("parameters", :parameters)
|
|
16
|
+
merge_parameters_with_outer(inner, outer)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def merge_parameters_with_outer(inner, outer)
|
|
20
|
+
inner.merge(outer) do |_key, inner_val, outer_val|
|
|
21
|
+
if inner_val.is_a?(Hash) && outer_val.is_a?(Hash)
|
|
22
|
+
merge_parameters_with_outer(inner_val, outer_val)
|
|
23
|
+
else
|
|
24
|
+
outer_val
|
|
25
|
+
end
|
|
26
|
+
end
|
|
16
27
|
end
|
|
17
28
|
|
|
18
29
|
def blank_tool_value?(value)
|
data/lib/ollama_agent/version.rb
CHANGED
data/lib/ollama_agent.rb
CHANGED
|
@@ -6,6 +6,13 @@ require_relative "ollama_agent/console"
|
|
|
6
6
|
require_relative "ollama_agent/agent"
|
|
7
7
|
require_relative "ollama_agent/cli"
|
|
8
8
|
|
|
9
|
+
# Public namespace for the Ollama-backed coding agent gem (CLI, Agent, tools, self-improvement helpers).
|
|
9
10
|
module OllamaAgent
|
|
10
11
|
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
def self.gem_root
|
|
14
|
+
File.expand_path("..", __dir__)
|
|
15
|
+
end
|
|
11
16
|
end
|
|
17
|
+
|
|
18
|
+
require_relative "ollama_agent/self_improvement"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ollama_agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shubham Taywade
|
|
@@ -134,6 +134,7 @@ files:
|
|
|
134
134
|
- lib/ollama_agent/console.rb
|
|
135
135
|
- lib/ollama_agent/diff_path_validator.rb
|
|
136
136
|
- lib/ollama_agent/ollama_connection.rb
|
|
137
|
+
- lib/ollama_agent/patch_risk.rb
|
|
137
138
|
- lib/ollama_agent/patch_support.rb
|
|
138
139
|
- lib/ollama_agent/repo_list.rb
|
|
139
140
|
- lib/ollama_agent/ruby_index.rb
|
|
@@ -145,6 +146,10 @@ files:
|
|
|
145
146
|
- lib/ollama_agent/ruby_index_tool_support.rb
|
|
146
147
|
- lib/ollama_agent/ruby_search_modes.rb
|
|
147
148
|
- lib/ollama_agent/sandboxed_tools.rb
|
|
149
|
+
- lib/ollama_agent/self_improvement.rb
|
|
150
|
+
- lib/ollama_agent/self_improvement/analyzer.rb
|
|
151
|
+
- lib/ollama_agent/self_improvement/improver.rb
|
|
152
|
+
- lib/ollama_agent/self_improvement/modes.rb
|
|
148
153
|
- lib/ollama_agent/think_param.rb
|
|
149
154
|
- lib/ollama_agent/timeout_param.rb
|
|
150
155
|
- lib/ollama_agent/tool_arguments.rb
|