agent-harness 0.5.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22137a8f8e81a58503c1e19a2349c6a8e1c8617cc5e12a38470fdc6477b483de
4
- data.tar.gz: 185d6003da7d94edfc5fc48cb55f4916b3ca06601bed95de5cfe3db88d0d49bb
3
+ metadata.gz: 16862141d853f2e8817d000a9e4813162f0544cda2c30343de2efc1ffd7e9e73
4
+ data.tar.gz: ee57bd3611abb7566560675c65c1feb04faee531cd182f8a8812881a73701aae
5
5
  SHA512:
6
- metadata.gz: 030b8e627328572ad8d01c5245a6f21b2e3070be6255505eb6e6a3df3a2040083218ee0b28c36dd0584622604fd93319ea6e7675308f2d7212007a4b7369320a
7
- data.tar.gz: 23cb19b897faf13438bfdc875c2f151eadcefa13c704cf9f80d443db3e99b7317856d9d744514b082a9f2abd813a26c7dcbf84f243c3e35879f564f979fe180f
6
+ metadata.gz: f5145b28d2c92bef3c8ba6f8a0e8ffe2217f8eb9462c672f28d25e0f6bd6a1bb17f1fa14ae2dcf2e57d35c4fa82a6233fdf51acf7778d399c89c4412ee9c9695
7
+ data.tar.gz: f6f9e464d3f5f84a87f98b98dd3d0687157a1b69249a3a0731ba162653aabf6cc517430f31b0f9e9b1d3e5d67d0b767c4c5a12c4e88425bd8b24f578343a87e9
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.5.0"
2
+ ".": "0.5.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.1](https://github.com/viamin/agent-harness/compare/agent-harness/v0.5.0...agent-harness/v0.5.1) (2026-03-24)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * 30: fix(codex): use 'codex exec' subcommand instead of --prompt ([#35](https://github.com/viamin/agent-harness/issues/35)) ([1093a23](https://github.com/viamin/agent-harness/commit/1093a23dd001a7ea3caf13306d284fe3b5b976c5))
9
+ * **anthropic:** use positional argument instead of --prompt for Claude CLI ([4ba59bd](https://github.com/viamin/agent-harness/commit/4ba59bd55394cf9ff1d1994ce787e0e285725b93)), closes [#29](https://github.com/viamin/agent-harness/issues/29)
10
+ * **kilocode:** use 'kilo run' subcommand instead of --prompt flag ([f850f54](https://github.com/viamin/agent-harness/commit/f850f54cfac595fe910298303beb373c7bc68376))
11
+ * **test:** use correct RSpec matcher `end_with` instead of `ending_with` ([3a9d68b](https://github.com/viamin/agent-harness/commit/3a9d68b90a0e788683a382303108ebe28cc24e63))
12
+
3
13
  ## [0.5.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.4.0...agent-harness/v0.5.0) (2026-03-03)
4
14
 
5
15
 
data/README.md CHANGED
@@ -326,6 +326,36 @@ Any existing expiry metadata in the credentials file is cleared on refresh so th
326
326
 
327
327
  This raises `NotImplementedError` for `:api_key` providers. Credential file paths respect the `CLAUDE_CONFIG_DIR` environment variable.
328
328
 
329
+ ## Provider Health Checks
330
+
331
+ Pre-flight check that configured providers are registered and authenticated. Reachability and configuration validation depend on provider-specific `health_status` and `validate_config` overrides; providers that don't implement these use safe defaults (healthy / valid).
332
+
333
+ > **Note:** These methods provide the library-level API. CLI flag (`--check-providers`) and HTTP endpoint (`GET /providers/status`) integration are not yet implemented and are tracked separately.
334
+
335
+ ```ruby
336
+ # Check all enabled providers
337
+ results = AgentHarness.check_providers
338
+ results.each do |r|
339
+ puts "#{r[:name]}: #{r[:status]} - #{r[:message]} (#{r[:latency_ms]}ms)"
340
+ end
341
+
342
+ # Check a single provider
343
+ result = AgentHarness.check_provider(:claude)
344
+ puts result[:status] # => "ok", "degraded", or "error"
345
+
346
+ # Formatted CLI output
347
+ puts AgentHarness::ProviderHealthCheck.format_results(results)
348
+ ```
349
+
350
+ Each result is a hash with keys:
351
+
352
+ - `:name` — provider name (Symbol)
353
+ - `:status` — `"ok"` (all checks passed), `"degraded"` (partial issues such as unimplemented auth status), or `"error"` (provider unavailable or authentication failed)
354
+ - `:message` — human-readable description
355
+ - `:latency_ms` — time taken for the check in milliseconds
356
+
357
+ Health checks run five steps per provider: registration, CLI availability, authentication, provider health status, and configuration validation. The default timeout per provider is configurable via `orchestration.health_check.timeout` (default: 5 seconds).
358
+
329
359
  ## Development
330
360
 
331
361
  ```bash
data/json-2.18.1.gem ADDED
Binary file
@@ -221,12 +221,13 @@ module AgentHarness
221
221
 
222
222
  # Health check configuration
223
223
  class HealthCheckConfig
224
- attr_accessor :enabled, :interval, :failure_threshold
224
+ attr_accessor :enabled, :interval, :failure_threshold, :timeout
225
225
 
226
226
  def initialize
227
227
  @enabled = true
228
228
  @interval = 60 # 1 minute
229
229
  @failure_threshold = 3
230
+ @timeout = 5 # seconds per provider check
230
231
  end
231
232
  end
232
233
 
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ module AgentHarness
6
+ # Performs health checks on configured providers
7
+ #
8
+ # Validates provider setup, authentication status, and reachability.
9
+ # Returns per-provider status objects with name, status, message, and latency.
10
+ #
11
+ # @example Check all providers
12
+ # results = AgentHarness::ProviderHealthCheck.check_all
13
+ # results.each { |r| puts "#{r[:name]}: #{r[:status]}" }
14
+ #
15
+ # @example Check a single provider
16
+ # result = AgentHarness::ProviderHealthCheck.check(:claude)
17
+ # puts result[:status] # => "ok", "error", or "degraded"
18
+ class ProviderHealthCheck
19
+ # Single source of truth: derive the fallback from HealthCheckConfig's default
20
+ # so that the timeout isn't duplicated here and in configuration.rb.
21
+ DEFAULT_TIMEOUT = HealthCheckConfig.new.timeout
22
+
23
+ class << self
24
+ # Check health of all configured providers
25
+ #
26
+ # @param timeout [Integer] timeout in seconds for each check
27
+ # @return [Array<Hash>] health status for each provider
28
+ def check_all(timeout: configured_timeout)
29
+ provider_names = if AgentHarness.configuration.providers.empty?
30
+ Providers::Registry.instance.all
31
+ else
32
+ enabled_provider_names
33
+ end
34
+
35
+ provider_names.map { |name| check(name, timeout: timeout) }
36
+ end
37
+
38
+ # Check health of a single provider
39
+ #
40
+ # @param provider_name [Symbol, String] the provider name
41
+ # @param timeout [Integer] timeout in seconds
42
+ # @return [Hash] health status with :name, :status, :message, :latency_ms keys
43
+ def check(provider_name, timeout: configured_timeout)
44
+ name = normalize_name(provider_name)
45
+ start_time = monotonic_now
46
+ timeout = validate_timeout(timeout)
47
+
48
+ Timeout.timeout(timeout) do
49
+ perform_check(name, start_time)
50
+ end
51
+ rescue Timeout::Error
52
+ build_result(
53
+ name: name,
54
+ status: "error",
55
+ message: "Health check timed out after #{timeout}s",
56
+ start_time: start_time || monotonic_now
57
+ )
58
+ rescue NotImplementedError => e
59
+ # NotImplementedError inherits from ScriptError, not StandardError,
60
+ # so it must be rescued explicitly. Its messages are safe internal
61
+ # setup errors (e.g., missing provider methods) that help users
62
+ # diagnose configuration problems.
63
+ AgentHarness.logger&.error("ProviderHealthCheck error for #{name}: #{e.class}")
64
+ build_result(
65
+ name: name,
66
+ status: "error",
67
+ message: "Health check failed: #{e.class}: #{e.message}",
68
+ start_time: start_time || monotonic_now
69
+ )
70
+ rescue => e
71
+ # Return a generic message to avoid leaking sensitive details
72
+ # (e.g., tokens embedded in exception messages). Log only the
73
+ # exception class (not the message) to avoid leaking secrets.
74
+ AgentHarness.logger&.error("ProviderHealthCheck error for #{name}: #{e.class}")
75
+ build_result(
76
+ name: name,
77
+ status: "error",
78
+ message: "Health check failed: #{e.class}",
79
+ start_time: start_time || monotonic_now
80
+ )
81
+ end
82
+
83
+ # Format health check results for CLI output
84
+ #
85
+ # @param results [Array<Hash>] health check results
86
+ # @return [String] formatted output
87
+ def format_results(results)
88
+ lines = ["Checking providers..."]
89
+
90
+ if results.empty?
91
+ lines << ""
92
+ lines << "No providers checked."
93
+ return lines.join("\n")
94
+ end
95
+
96
+ results.each do |result|
97
+ name = result[:name].to_s.ljust(16)
98
+ case result[:status]
99
+ when "ok"
100
+ latency = result[:latency_ms] ? "(#{result[:latency_ms]}ms)" : ""
101
+ lines << " ✓ #{name} OK #{latency}".rstrip
102
+ when "degraded"
103
+ lines << " ~ #{name} #{result[:message]}"
104
+ else
105
+ lines << " ✗ #{name} #{result[:message]}"
106
+ end
107
+ end
108
+
109
+ failed = results.count { |r| r[:status] == "error" }
110
+ degraded = results.count { |r| r[:status] == "degraded" }
111
+ total = results.size
112
+
113
+ lines << ""
114
+ summary_parts = []
115
+ summary_parts << "#{failed} failed" if failed > 0
116
+ summary_parts << "#{degraded} degraded" if degraded > 0
117
+
118
+ provider_word = (total == 1) ? "provider" : "providers"
119
+ lines << if summary_parts.any?
120
+ "#{total} #{provider_word} checked: #{summary_parts.join(", ")}."
121
+ else
122
+ "All #{total} #{provider_word} healthy."
123
+ end
124
+
125
+ lines.join("\n")
126
+ end
127
+
128
+ private
129
+
130
+ def enabled_provider_names
131
+ AgentHarness.configuration.providers.select { |_name, config| config.enabled }.keys
132
+ end
133
+
134
+ def validate_timeout(timeout)
135
+ (timeout.is_a?(Numeric) && timeout.positive?) ? timeout : configured_timeout
136
+ end
137
+
138
+ def configured_timeout
139
+ timeout = AgentHarness.configuration.orchestration_config.health_check_config.timeout
140
+ (timeout.is_a?(Numeric) && timeout.positive?) ? timeout : DEFAULT_TIMEOUT
141
+ rescue NoMethodError
142
+ DEFAULT_TIMEOUT
143
+ end
144
+
145
+ def normalize_name(provider_name)
146
+ provider_name.to_sym
147
+ rescue NoMethodError, ArgumentError, TypeError
148
+ :unknown
149
+ end
150
+
151
+ def perform_check(provider_name, start_time)
152
+ # Step 1: Check provider is registered
153
+ registry = Providers::Registry.instance
154
+ unless registry.registered?(provider_name)
155
+ return build_result(
156
+ name: provider_name,
157
+ status: "error",
158
+ message: "Provider not registered",
159
+ start_time: start_time
160
+ )
161
+ end
162
+
163
+ # Step 2: Check CLI availability
164
+ klass = registry.get(provider_name)
165
+ unless klass.available?
166
+ return build_result(
167
+ name: provider_name,
168
+ status: "error",
169
+ message: "CLI '#{klass.binary_name}' not found in PATH",
170
+ start_time: start_time
171
+ )
172
+ end
173
+
174
+ # Step 3: Check authentication
175
+ # Treat "not implemented" auth status as degraded rather than error,
176
+ # since most built-in providers don't implement auth_status hooks.
177
+ # In either case, continue to steps 4/5 so health and config issues
178
+ # are still surfaced for providers that lack an auth_status hook.
179
+ auth = Authentication.auth_status(provider_name)
180
+ auth_degraded = false
181
+ unless auth[:valid]
182
+ unless auth_not_implemented?(auth)
183
+ return build_result(
184
+ name: provider_name,
185
+ status: "error",
186
+ message: auth[:error] || "Authentication failed",
187
+ start_time: start_time
188
+ )
189
+ end
190
+ auth_degraded = true
191
+ end
192
+
193
+ # Step 4: Check provider-level health (e.g., endpoint reachability)
194
+ # The Adapter default always returns {healthy: true}, so providers
195
+ # that haven't implemented a real health check are reported as ok
196
+ # with a note that the check is not implemented.
197
+ provider_instance = build_provider(provider_name, klass)
198
+ health = provider_instance.health_status
199
+ unless health[:healthy]
200
+ return build_result(
201
+ name: provider_name,
202
+ status: "degraded",
203
+ message: health[:message] || "Provider health check failed",
204
+ start_time: start_time
205
+ )
206
+ end
207
+
208
+ # Step 5: Validate provider config
209
+ # The Adapter default always returns {valid: true}, so providers
210
+ # that haven't implemented real config validation pass by default.
211
+ validation = provider_instance.validate_config
212
+ unless validation[:valid]
213
+ errors_msg = Array(validation[:errors]).join(", ")
214
+ errors_msg = "check provider configuration" if errors_msg.empty?
215
+ return build_result(
216
+ name: provider_name,
217
+ status: "degraded",
218
+ message: "Configuration issues: #{errors_msg}",
219
+ start_time: start_time
220
+ )
221
+ end
222
+
223
+ # If auth was not implemented but health/config passed, report degraded
224
+ if auth_degraded
225
+ return build_result(
226
+ name: provider_name,
227
+ status: "degraded",
228
+ message: "Auth status check not implemented; health and config checks passed",
229
+ start_time: start_time
230
+ )
231
+ end
232
+
233
+ message = if provider_overrides_method?(provider_instance, :health_status) ||
234
+ provider_overrides_method?(provider_instance, :validate_config)
235
+ "All checks passed"
236
+ else
237
+ "Registered and authenticated (health/config checks use defaults)"
238
+ end
239
+
240
+ build_result(
241
+ name: provider_name,
242
+ status: "ok",
243
+ message: message,
244
+ start_time: start_time
245
+ )
246
+ end
247
+
248
+ def auth_not_implemented?(auth)
249
+ # Prefer explicit flags over brittle string matching on error messages.
250
+ # This keeps backward compatibility with existing callers that only set :error,
251
+ # while allowing newer callers to pass structured reasons.
252
+ if auth.respond_to?(:[])
253
+ return true if auth.key?(:implemented) && auth[:implemented] == false
254
+ return true if auth.key?(:reason) && auth[:reason] == :not_implemented
255
+ end
256
+
257
+ error = auth[:error].to_s
258
+ error.include?("not implemented")
259
+ end
260
+
261
+ def provider_overrides_method?(provider_instance, method_name)
262
+ provider_instance.method(method_name).owner != Providers::Adapter
263
+ end
264
+
265
+ def build_result(name:, status:, message:, start_time:)
266
+ latency = ((monotonic_now - start_time) * 1000).round
267
+ {
268
+ name: name,
269
+ status: status,
270
+ message: message,
271
+ latency_ms: latency
272
+ }
273
+ end
274
+
275
+ def build_provider(provider_name, klass)
276
+ config = AgentHarness.configuration.providers[provider_name]
277
+ klass.new(
278
+ config: config,
279
+ executor: AgentHarness.configuration.command_executor,
280
+ logger: AgentHarness.logger
281
+ )
282
+ end
283
+
284
+ def monotonic_now
285
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
286
+ end
287
+ end
288
+ end
289
+ end
@@ -269,7 +269,7 @@ module AgentHarness
269
269
  # Add custom flags from config
270
270
  cmd += @config.default_flags if @config.default_flags&.any?
271
271
 
272
- cmd += ["--prompt", prompt]
272
+ cmd << prompt
273
273
 
274
274
  cmd
275
275
  end
@@ -81,13 +81,13 @@ module AgentHarness
81
81
  protected
82
82
 
83
83
  def build_command(prompt, options)
84
- cmd = [self.class.binary_name]
84
+ cmd = [self.class.binary_name, "exec"]
85
85
 
86
86
  if options[:session]
87
87
  cmd += session_flags(options[:session])
88
88
  end
89
89
 
90
- cmd += ["--prompt", prompt]
90
+ cmd << prompt
91
91
 
92
92
  cmd
93
93
  end
@@ -12,7 +12,7 @@ module AgentHarness
12
12
  end
13
13
 
14
14
  def binary_name
15
- "kilocode"
15
+ "kilo"
16
16
  end
17
17
 
18
18
  def available?
@@ -60,8 +60,8 @@ module AgentHarness
60
60
  protected
61
61
 
62
62
  def build_command(prompt, options)
63
- cmd = [self.class.binary_name]
64
- cmd += ["--prompt", prompt]
63
+ cmd = [self.class.binary_name, "run"]
64
+ cmd << prompt
65
65
  cmd
66
66
  end
67
67
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.1"
5
5
  end
data/lib/agent_harness.rb CHANGED
@@ -113,6 +113,25 @@ module AgentHarness
113
113
  def refresh_auth(provider_name, token: nil)
114
114
  Authentication.refresh_auth(provider_name, token: token)
115
115
  end
116
+
117
+ # Check health of all configured providers.
118
+ #
119
+ # Validates each enabled provider through registration, CLI availability,
120
+ # authentication, provider health status, and config validation checks.
121
+ #
122
+ # @param timeout [Integer] timeout in seconds for each check (defaults to configured value)
123
+ # @return [Array<Hash>] health status for each provider
124
+ def check_providers(timeout: nil)
125
+ timeout ? ProviderHealthCheck.check_all(timeout: timeout) : ProviderHealthCheck.check_all
126
+ end
127
+
128
+ # Check health of a single provider
129
+ # @param provider_name [Symbol] the provider name
130
+ # @param timeout [Integer, nil] timeout in seconds (nil lets ProviderHealthCheck apply its validated default)
131
+ # @return [Hash] health status with :name, :status, :message, :latency_ms
132
+ def check_provider(provider_name, timeout: nil)
133
+ timeout ? ProviderHealthCheck.check(provider_name, timeout: timeout) : ProviderHealthCheck.check(provider_name)
134
+ end
116
135
  end
117
136
  end
118
137
 
@@ -125,6 +144,7 @@ require_relative "agent_harness/response"
125
144
  require_relative "agent_harness/token_tracker"
126
145
  require_relative "agent_harness/error_taxonomy"
127
146
  require_relative "agent_harness/authentication"
147
+ require_relative "agent_harness/provider_health_check"
128
148
 
129
149
  # Provider layer
130
150
  require_relative "agent_harness/providers/registry"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agent-harness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan
@@ -76,6 +76,7 @@ files:
76
76
  - Rakefile
77
77
  - bin/console
78
78
  - bin/setup
79
+ - json-2.18.1.gem
79
80
  - lib/agent_harness.rb
80
81
  - lib/agent_harness/authentication.rb
81
82
  - lib/agent_harness/command_executor.rb
@@ -89,6 +90,7 @@ files:
89
90
  - lib/agent_harness/orchestration/metrics.rb
90
91
  - lib/agent_harness/orchestration/provider_manager.rb
91
92
  - lib/agent_harness/orchestration/rate_limiter.rb
93
+ - lib/agent_harness/provider_health_check.rb
92
94
  - lib/agent_harness/providers/adapter.rb
93
95
  - lib/agent_harness/providers/aider.rb
94
96
  - lib/agent_harness/providers/anthropic.rb
@@ -126,7 +128,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
128
  - !ruby/object:Gem::Version
127
129
  version: '0'
128
130
  requirements: []
129
- rubygems_version: 4.0.3
131
+ rubygems_version: 4.0.6
130
132
  specification_version: 4
131
133
  summary: Unified interface for CLI-based AI coding agents
132
134
  test_files: []