ask-ruby-harness 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.md +67 -0
- data/lib/ask/ruby/harness/audit_log.rb +187 -0
- data/lib/ask/ruby/harness/configuration.rb +77 -0
- data/lib/ask/ruby/harness/environment_permissions.rb +41 -0
- data/lib/ask/ruby/harness/minitest_json_reporter.rb +84 -0
- data/lib/ask/ruby/harness/tool.rb +46 -0
- data/lib/ask/ruby/harness/tools/query_database.rb +87 -0
- data/lib/ask/ruby/harness/tools/read_log.rb +103 -0
- data/lib/ask/ruby/harness/tools/read_model.rb +77 -0
- data/lib/ask/ruby/harness/tools/run_command.rb +60 -0
- data/lib/ask/ruby/harness/tools/run_tests.rb +262 -0
- data/lib/ask/ruby/harness/tools/schema_graph.rb +179 -0
- data/lib/ask/ruby/harness/version.rb +9 -0
- data/lib/ask/ruby/harness.rb +216 -0
- data/lib/minitest/ask_ruby_harness_plugin.rb +39 -0
- metadata +177 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ask/agent"
|
|
4
|
+
require "time"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
module Ask
|
|
8
|
+
module Ruby
|
|
9
|
+
module Harness
|
|
10
|
+
class << self
|
|
11
|
+
def configure
|
|
12
|
+
yield configuration
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def configuration
|
|
16
|
+
@configuration ||= Configuration.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The project root the harness serves. Defaults to the current working
|
|
20
|
+
# directory; framework editions set it to the framework's root at boot
|
|
21
|
+
# (ask-rails-harness sets it to ::Rails.root).
|
|
22
|
+
def app_root
|
|
23
|
+
@app_root ||= Pathname.new(Dir.pwd)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def app_root=(path)
|
|
27
|
+
@app_root = path ? Pathname.new(path) : nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Environment detection without Rails: RAILS_ENV, RACK_ENV, APP_ENV.
|
|
31
|
+
def env
|
|
32
|
+
ENV["RAILS_ENV"] || ENV["RACK_ENV"] || ENV["APP_ENV"] || "development"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def agent_session(**extra)
|
|
36
|
+
# Auto-prune if configured
|
|
37
|
+
cleanup! if configuration.max_session_age || configuration.max_sessions
|
|
38
|
+
|
|
39
|
+
tools = configuration.tools.map { |t| t.is_a?(Class) ? t.new : t }
|
|
40
|
+
prompt = extra.delete(:system_prompt) || configuration.system_prompt || default_system_prompt
|
|
41
|
+
|
|
42
|
+
# Resolve environment-specific permissions and wire into agent hooks
|
|
43
|
+
hooks = build_environment_hooks
|
|
44
|
+
|
|
45
|
+
Ask::Agent::Session.new(
|
|
46
|
+
model: configuration.default_model,
|
|
47
|
+
max_turns: configuration.max_turns,
|
|
48
|
+
system_prompt: prompt,
|
|
49
|
+
tools: tools,
|
|
50
|
+
state: configuration.persistence_adapter,
|
|
51
|
+
hooks: hooks,
|
|
52
|
+
**extra
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def discover_tools!
|
|
57
|
+
self.configuration.tools = Ask::Tools::Shell::TOOLS.map(&:new) + harness_tools + discovered_user_tools
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Prune old sessions and audit logs based on configuration limits.
|
|
61
|
+
#
|
|
62
|
+
# Removes sessions older than +max_session_age+ seconds, and limits the
|
|
63
|
+
# total number of sessions to +max_sessions+ (deleting the oldest first).
|
|
64
|
+
# Audit log entries older than the oldest kept session are also removed.
|
|
65
|
+
def cleanup!
|
|
66
|
+
prune_old_sessions
|
|
67
|
+
limit_session_count
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Establish a standalone ActiveRecord connection when the host hasn't
|
|
71
|
+
# (Rails apps are already connected and are left untouched).
|
|
72
|
+
#
|
|
73
|
+
# Connection sources, in order: ASK_DATABASE_URL, config/database.yml.
|
|
74
|
+
def connect_database!
|
|
75
|
+
return if database_connected?
|
|
76
|
+
|
|
77
|
+
config = ENV["ASK_DATABASE_URL"] || database_config_from_yaml
|
|
78
|
+
ActiveRecord::Base.establish_connection(config) if config
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
warn "[ask-ruby-harness] database connect failed: #{e.message}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def database_connected?
|
|
84
|
+
defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def build_environment_hooks
|
|
90
|
+
env_mode = configuration.effective_mode
|
|
91
|
+
return {} unless env_mode
|
|
92
|
+
|
|
93
|
+
perms = Ask::Agent::Policies::Permissions.new(mode: env_mode)
|
|
94
|
+
{ before_tool: [perms.method(:before_tool_call)] }
|
|
95
|
+
rescue ArgumentError => e
|
|
96
|
+
warn "[ask-ruby-harness] Invalid environment mode: #{e.message}"
|
|
97
|
+
{}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def prune_old_sessions
|
|
101
|
+
age = configuration.max_session_age
|
|
102
|
+
return unless age&.> 0
|
|
103
|
+
return unless persistence_available?
|
|
104
|
+
|
|
105
|
+
cutoff = age.seconds.ago
|
|
106
|
+
count = 0
|
|
107
|
+
|
|
108
|
+
configuration.persistence_adapter.list.each do |id|
|
|
109
|
+
data = configuration.persistence_adapter.load(id)
|
|
110
|
+
created = data&.dig(:metadata, :created_at)
|
|
111
|
+
if created && Time.parse(created) < cutoff
|
|
112
|
+
configuration.persistence_adapter.delete(id)
|
|
113
|
+
count += 1
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
count
|
|
118
|
+
rescue StandardError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def limit_session_count
|
|
123
|
+
max = configuration.max_sessions
|
|
124
|
+
return unless max&.> 0
|
|
125
|
+
return unless persistence_available?
|
|
126
|
+
|
|
127
|
+
sessions = configuration.persistence_adapter.list
|
|
128
|
+
excess = sessions.size - max
|
|
129
|
+
return unless excess > 0
|
|
130
|
+
|
|
131
|
+
# Delete oldest sessions first
|
|
132
|
+
with_timestamps = sessions.map { |id|
|
|
133
|
+
data = configuration.persistence_adapter.load(id)
|
|
134
|
+
created = data&.dig(:metadata, :created_at)
|
|
135
|
+
[id, created ? Time.parse(created) : Time.at(0)]
|
|
136
|
+
}.sort_by(&:last)
|
|
137
|
+
|
|
138
|
+
with_timestamps.first(excess).each do |id, _|
|
|
139
|
+
configuration.persistence_adapter.delete(id)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
excess
|
|
143
|
+
rescue StandardError
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def persistence_available?
|
|
148
|
+
defined?(ActiveRecord::Base) &&
|
|
149
|
+
ActiveRecord::Base.connection.data_source_exists?("ask_sessions")
|
|
150
|
+
rescue StandardError
|
|
151
|
+
false
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def harness_tools
|
|
155
|
+
HARNESS_TOOLS.map(&:new)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def discovered_user_tools
|
|
159
|
+
tools = []
|
|
160
|
+
files = Dir[app_root.join("app", "tools", "*.rb")]
|
|
161
|
+
files.each do |f|
|
|
162
|
+
require f
|
|
163
|
+
klass = File.basename(f, ".rb").camelize.constantize rescue next
|
|
164
|
+
tools << klass if klass < Ask::Ruby::Harness::Tool
|
|
165
|
+
end
|
|
166
|
+
tools
|
|
167
|
+
rescue
|
|
168
|
+
tools
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def database_config_from_yaml
|
|
172
|
+
path = app_root.join("config", "database.yml")
|
|
173
|
+
return nil unless path.exist?
|
|
174
|
+
|
|
175
|
+
yaml = YAML.safe_load(path.read, aliases: true) || {}
|
|
176
|
+
section = yaml[env] || yaml["development"] || {}
|
|
177
|
+
# Rails-style multi-database config nests the primary under a key.
|
|
178
|
+
section = section["primary"] if section.is_a?(Hash) && section["primary"].is_a?(Hash)
|
|
179
|
+
return nil unless section.is_a?(Hash)
|
|
180
|
+
|
|
181
|
+
section.slice(*Configuration::DATABASE_CONFIG_KEYS).presence
|
|
182
|
+
rescue Psych::Exception
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def default_system_prompt
|
|
187
|
+
<<~PROMPT
|
|
188
|
+
You are a Ruby software engineer.
|
|
189
|
+
You have direct access to the project's code, database, and runtime.
|
|
190
|
+
Use your tools to inspect and modify the codebase.
|
|
191
|
+
Once you have enough information, stop calling tools and give your answer.
|
|
192
|
+
PROMPT
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
require_relative "harness/version"
|
|
200
|
+
require_relative "harness/configuration"
|
|
201
|
+
require_relative "harness/audit_log"
|
|
202
|
+
require_relative "harness/environment_permissions"
|
|
203
|
+
require_relative "harness/tool"
|
|
204
|
+
require_relative "harness/tools/run_command"
|
|
205
|
+
require_relative "harness/tools/query_database"
|
|
206
|
+
require_relative "harness/tools/read_model"
|
|
207
|
+
require_relative "harness/tools/read_log"
|
|
208
|
+
require_relative "harness/tools/schema_graph"
|
|
209
|
+
require_relative "harness/tools/run_tests"
|
|
210
|
+
|
|
211
|
+
# Define after all tool files are loaded so the constants resolve
|
|
212
|
+
Ask::Ruby::Harness::HARNESS_TOOLS = [
|
|
213
|
+
Ask::Ruby::Harness::Tools::RunCommand, Ask::Ruby::Harness::Tools::QueryDatabase,
|
|
214
|
+
Ask::Ruby::Harness::Tools::ReadModel, Ask::Ruby::Harness::Tools::ReadLog,
|
|
215
|
+
Ask::Ruby::Harness::Tools::SchemaGraph, Ask::Ruby::Harness::Tools::RunTests
|
|
216
|
+
].freeze
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Minitest plugin for ask-ruby-harness.
|
|
4
|
+
#
|
|
5
|
+
# Registers the JSON reporter when a run was started by the harness's
|
|
6
|
+
# run_tests tool (ASK_TEST_JSON_PATH set); ordinary test runs are untouched.
|
|
7
|
+
#
|
|
8
|
+
# Loading differs by minitest version:
|
|
9
|
+
# - minitest 5 auto-discovers `minitest/*_plugin.rb` files and calls
|
|
10
|
+
# init_plugins with `#{name}_plugin_init`.
|
|
11
|
+
# - minitest 6 dropped auto-discovery; the run_tests tool injects this file
|
|
12
|
+
# via RUBYOPT="-r <absolute path>" and init_plugins dispatches
|
|
13
|
+
# `plugin_#{name}_init` instead.
|
|
14
|
+
# Either way, pushing the extension below (idempotent) makes init_plugins
|
|
15
|
+
# dispatch to the matching init method after the composite reporter exists —
|
|
16
|
+
# the only reliable point to append a reporter.
|
|
17
|
+
# __dir__-based requires keep the file loadable before Bundler.setup.
|
|
18
|
+
require File.expand_path("../ask/ruby/harness/minitest_json_reporter", __dir__)
|
|
19
|
+
|
|
20
|
+
module Minitest
|
|
21
|
+
# Minitest 6 convention: init_plugins calls plugin_#{name}_init.
|
|
22
|
+
def self.plugin_ask_ruby_harness_init(options)
|
|
23
|
+
register_ask_ruby_harness_json_reporter(options)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Minitest 5 convention: init_plugins calls #{name}_plugin_init.
|
|
27
|
+
def self.ask_ruby_harness_plugin_init(options)
|
|
28
|
+
register_ask_ruby_harness_json_reporter(options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.register_ask_ruby_harness_json_reporter(_options)
|
|
32
|
+
path = ENV["ASK_TEST_JSON_PATH"]
|
|
33
|
+
return if path.nil? || path.to_s.empty?
|
|
34
|
+
|
|
35
|
+
Minitest.reporter << Ask::Ruby::Harness::MinitestJsonReporter.new(path)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Minitest.extensions << "ask_ruby_harness" unless Minitest.extensions.include?("ask_ruby_harness")
|
metadata
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ask-ruby-harness
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kaka Ruto
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ask-tools
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: ask-tools-shell
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: ask-agent
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 0.28.0
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 0.28.0
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: sqlite3
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '2.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '2.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: minitest
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '5.25'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '5.25'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: mocha
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '3.1'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '3.1'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rake
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '13.0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '13.0'
|
|
124
|
+
description: Harness for AI coding agents working in any Ruby project. Ships a language-agnostic
|
|
125
|
+
tool contract (RunCommand, ReadLog, QueryDatabase, ReadModel, SchemaGraph, RunTests)
|
|
126
|
+
with structured returns, read-only DB enforcement, environment permissions, and
|
|
127
|
+
audit logging. The Rails edition (ask-rails-harness) adds framework-native tools
|
|
128
|
+
on top.
|
|
129
|
+
email:
|
|
130
|
+
- kaka@myrrlabs.com
|
|
131
|
+
executables: []
|
|
132
|
+
extensions: []
|
|
133
|
+
extra_rdoc_files: []
|
|
134
|
+
files:
|
|
135
|
+
- CHANGELOG.md
|
|
136
|
+
- LICENSE
|
|
137
|
+
- README.md
|
|
138
|
+
- lib/ask/ruby/harness.rb
|
|
139
|
+
- lib/ask/ruby/harness/audit_log.rb
|
|
140
|
+
- lib/ask/ruby/harness/configuration.rb
|
|
141
|
+
- lib/ask/ruby/harness/environment_permissions.rb
|
|
142
|
+
- lib/ask/ruby/harness/minitest_json_reporter.rb
|
|
143
|
+
- lib/ask/ruby/harness/tool.rb
|
|
144
|
+
- lib/ask/ruby/harness/tools/query_database.rb
|
|
145
|
+
- lib/ask/ruby/harness/tools/read_log.rb
|
|
146
|
+
- lib/ask/ruby/harness/tools/read_model.rb
|
|
147
|
+
- lib/ask/ruby/harness/tools/run_command.rb
|
|
148
|
+
- lib/ask/ruby/harness/tools/run_tests.rb
|
|
149
|
+
- lib/ask/ruby/harness/tools/schema_graph.rb
|
|
150
|
+
- lib/ask/ruby/harness/version.rb
|
|
151
|
+
- lib/minitest/ask_ruby_harness_plugin.rb
|
|
152
|
+
homepage: https://github.com/ask-rb/ask-ruby-harness
|
|
153
|
+
licenses:
|
|
154
|
+
- MIT
|
|
155
|
+
metadata:
|
|
156
|
+
homepage_uri: https://github.com/ask-rb/ask-ruby-harness
|
|
157
|
+
source_code_uri: https://github.com/ask-rb/ask-ruby-harness
|
|
158
|
+
changelog_uri: https://github.com/ask-rb/ask-ruby-harness/blob/master/CHANGELOG.md
|
|
159
|
+
rdoc_options: []
|
|
160
|
+
require_paths:
|
|
161
|
+
- lib
|
|
162
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
163
|
+
requirements:
|
|
164
|
+
- - ">="
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: '3.2'
|
|
167
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
168
|
+
requirements:
|
|
169
|
+
- - ">="
|
|
170
|
+
- !ruby/object:Gem::Version
|
|
171
|
+
version: '0'
|
|
172
|
+
requirements: []
|
|
173
|
+
rubygems_version: 4.0.3
|
|
174
|
+
specification_version: 4
|
|
175
|
+
summary: Admin AI copilot for Ruby projects — structured DB access, logs, commands,
|
|
176
|
+
and tests
|
|
177
|
+
test_files: []
|