Class: Space::Architect::Harness::OpenCodeHarness

Inherits:
Object
  • Object
show all
Defined in:
lib/space_architect/harness.rb

Constant Summary collapse

ACCEPTED_LEVELS =

opencode's reasoningEffort accepts only low/medium/high; off/minimal are stripped (omit reasoningEffort) and xhigh/max are clamped down to high.

%w[low medium high].freeze
CLAMP_MAP =
{ "xhigh" => "high", "max" => "high" }.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model:, max_turns:, bin: nil, config_dir:, effort: nil) ⇒ OpenCodeHarness

Returns a new instance of OpenCodeHarness.



317
318
319
320
321
322
323
# File 'lib/space_architect/harness.rb', line 317

def initialize(model:, max_turns:, bin: nil, config_dir:, effort: nil)
  @model      = model
  @max_turns  = max_turns
  @bin        = bin || ENV.fetch("ARCHITECT_OPENCODE_BIN", "opencode")
  @config_dir = Pathname.new(config_dir)
  @effort     = effort
end

Class Method Details

.translate_thinking(level, force: false) ⇒ Object



305
306
307
308
309
310
311
312
313
314
315
# File 'lib/space_architect/harness.rb', line 305

def self.translate_thinking(level, force: false)
  return [nil, nil] if level.nil?
  return [level, "thinking: force --effort=#{level} (unmodified, may be rejected)"] if force
  return [level, nil] if ACCEPTED_LEVELS.include?(level)

  if (clamped = CLAMP_MAP[level])
    [clamped, "thinking: #{level} → opencode #{clamped} (clamped; opencode accepts low/medium/high)"]
  else
    [nil, "thinking: #{level} → opencode (omitting reasoningEffort; opencode has no #{level} level)"]
  end
end

Instance Method Details

#builder_configObject

Returns the agent config hash (deterministic, unit-testable).



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/space_architect/harness.rb', line 326

def builder_config
  cfg = {
    "agent" => {
      "builder" => {
        "steps" => @max_turns,
        "permission" => {
          "bash" => {
            "git commit *"   => "deny",
            "git push *"     => "deny",
            "git reset *"    => "deny",
            "git rebase *"   => "deny",
            "git checkout *" => "deny",
            "*"              => "allow"
          }
        }
      }
    }
  }
  cfg.merge!(reasoning_provider_config) if @effort
  cfg
end

#run(prompt_path:, run_log_path:, chdir:, timeout: nil) ⇒ Object

timeout: deferred — opencode kill path is out of scope



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/space_architect/harness.rb', line 348

def run(prompt_path:, run_log_path:, chdir:, timeout: nil) # timeout: deferred — opencode kill path is out of scope
  prompt_path  = Pathname.new(prompt_path)
  run_log_path = Pathname.new(run_log_path)
  config_path  = write_config

  env = {
    "OPENCODE_CONFIG"                 => config_path.to_s,
    "OPENCODE_DISABLE_PROJECT_CONFIG" => "1"
  }

  File.open(prompt_path, "r") do |prompt_io|
    File.open(run_log_path, "w") do |log|
      status = Sync do
        Async::Process.spawn(env, *argv(chdir),
                             chdir: chdir.to_s, in: prompt_io, out: log, err: log)
      end
      status.exitstatus
    end
  end
end

#run_detached(prompt_path:, run_log_path:, chdir:) ⇒ Object



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'lib/space_architect/harness.rb', line 369

def run_detached(prompt_path:, run_log_path:, chdir:)
  prompt_path  = Pathname.new(prompt_path)
  run_log_path = Pathname.new(run_log_path)
  config_path  = write_config

  env = {
    "OPENCODE_CONFIG"                 => config_path.to_s,
    "OPENCODE_DISABLE_PROJECT_CONFIG" => "1"
  }

  prompt_io = File.open(prompt_path, "r")
  log       = File.open(run_log_path, "w")
  begin
    pid = Process.spawn(env, *argv(chdir), chdir: chdir.to_s, pgroup: true,
                        in: prompt_io, out: log, err: log)
    Process.detach(pid)
  ensure
    prompt_io.close
    log.close
  end
  pid
end