Module: Space::Architect::Harness

Defined in:
lib/space_architect/harness.rb

Defined Under Namespace

Classes: ClaudeCodeHarness, OpenCodeHarness, PiHarness

Constant Summary collapse

CLAUDE_DEFAULT_MODEL =
"claude-sonnet-5"
DEFAULT_MODELS =

Per-harness sensible default model, used when neither an explicit --model nor a space.yaml project.model is given.

{
  "claude-code" => CLAUDE_DEFAULT_MODEL,
  "pi"          => "qwen3-27b-optiq",
  "opencode"    => "fireworks-ai/accounts/fireworks/models/glm-5p2"
}.freeze
THINKING_LEVELS =

The normalized internal thinking vocabulary — pi's fullest set. Every alias (--effort/--thinking/--reasoning) is normalized to a member of this array; each harness then translates + clamps down to its own flag's vocabulary.

%w[off minimal low medium high xhigh max].freeze

Class Method Summary collapse

Class Method Details

.default_model_for(harness_name) ⇒ Object



23
24
25
# File 'lib/space_architect/harness.rb', line 23

def self.default_model_for(harness_name)
  DEFAULT_MODELS[harness_name.to_s]
end

.for(name, model:, max_turns:, bin: nil, config_dir: nil, effort: nil, force: false, err: $stderr) ⇒ Object

Factory keyed by harness name. Translates + clamps the normalized effort level to the harness's own vocabulary, printing one inform line to err (default $stderr; thread a null writer to suppress) when it clamps/strips. With force: true, the literal effort value is passed through unmodified. For opencode: config_dir is required (build/- dir outside the worktree).



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/space_architect/harness.rb', line 65

def self.for(name, model:, max_turns:, bin: nil, config_dir: nil, effort: nil, force: false, err: $stderr)
  translated, inform = translate_thinking(name, effort, force: force)
  err.puts(inform) if inform

  case name.to_s
  when "claude-code"
    ClaudeCodeHarness.new(model: model, max_turns: max_turns, bin: bin, effort: translated)
  when "opencode"
    raise Space::Core::Error, "config_dir is required for opencode harness" unless config_dir
    OpenCodeHarness.new(model: model, max_turns: max_turns, bin: bin, config_dir: config_dir, effort: translated)
  when "pi"
    raise Space::Core::Error, "config_dir is required for pi harness" unless config_dir
    PiHarness.new(model: model, max_turns: max_turns, bin: bin, config_dir: config_dir, effort: translated)
  else
    raise Space::Core::Error, "Unknown harness '#{name}' — valid values: claude-code, opencode, pi"
  end
end

.parse_model_suffix(model) ⇒ Object

Split a "model:level" id into [model, level] (level nil, model unchanged if no suffix). Only a trailing segment that is a member of THINKING_LEVELS is treated as a suffix — a model id containing a literal colon for another reason is left alone.



41
42
43
44
45
46
# File 'lib/space_architect/harness.rb', line 41

def self.parse_model_suffix(model)
  return [model, nil] unless model
  base, _, suffix = model.rpartition(":")
  return [model, nil] if base.empty? || !THINKING_LEVELS.include?(suffix)
  [base, suffix]
end

.translate_thinking(harness, level, force: false) ⇒ Object

Per-harness translate + clamp: input a normalized level (or nil) and whether the caller forced it (skips clamping — the literal is passed through unmodified). Returns [translated_level_or_nil, inform_line_or_nil].



51
52
53
54
55
56
57
58
# File 'lib/space_architect/harness.rb', line 51

def self.translate_thinking(harness, level, force: false)
  case harness.to_s
  when "claude-code" then ClaudeCodeHarness.translate_thinking(level, force: force)
  when "opencode"    then OpenCodeHarness.translate_thinking(level, force: force)
  when "pi"          then PiHarness.translate_thinking(level, force: force)
  else nil
  end
end

.validate_thinking_level!(level) ⇒ Object

Raises:



32
33
34
35
36
# File 'lib/space_architect/harness.rb', line 32

def self.validate_thinking_level!(level)
  return if level.nil? || THINKING_LEVELS.include?(level)
  raise Space::Core::Error,
    "unknown thinking level '#{level}' — valid: #{THINKING_LEVELS.join(', ')}"
end