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.
@@ -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: []