Class: Ocak::ClaudeRunner

Inherits:
Object
  • Object
show all
Defined in:
lib/ocak/claude_runner.rb

Defined Under Namespace

Classes: AgentResult

Constant Summary collapse

FailedStatus =

Backwards-compat alias — FailedStatus now lives in ProcessRunner.

ProcessRunner::FailedStatus
AGENT_TOOLS =
{
  'implementer' => 'Read,Write,Edit,Glob,Grep,Bash',
  'reviewer' => 'Read,Grep,Glob,Bash',
  'security-reviewer' => 'Read,Grep,Glob,Bash',
  'auditor' => 'Read,Grep,Glob,Bash',
  'documenter' => 'Read,Write,Edit,Glob,Grep,Bash',
  'merger' => 'Read,Glob,Grep,Bash',
  'pipeline' => 'Read,Write,Edit,Glob,Grep,Bash',
  'planner' => 'Read,Glob,Grep,Bash'
}.freeze
MODEL_HAIKU =
ENV.fetch('OCAK_MODEL_HAIKU',  'haiku')
MODEL_SONNET =
ENV.fetch('OCAK_MODEL_SONNET', 'sonnet')
MODEL_OPUS =
ENV.fetch('OCAK_MODEL_OPUS',   'opus')
AGENT_MODELS =
{
  'planner' => MODEL_SONNET,
  'reviewer' => MODEL_OPUS,
  'security-reviewer' => MODEL_SONNET,
  'auditor' => MODEL_SONNET,
  'documenter' => MODEL_SONNET,
  'merger' => MODEL_SONNET,
  'implementer' => MODEL_SONNET,
  'pipeline' => MODEL_OPUS
}.freeze
TIMEOUT =

10 minutes per agent invocation

600
MAX_RETRIES =
2
RETRY_DELAYS =
[5, 15].freeze
TRANSIENT_PATTERNS =
[
  /connection.*reset/i,
  /timed?\s*out/i,
  /ECONNREFUSED/,
  /rate\s*limit/i,
  /503|502|429/,
  /overloaded/i
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(config:, logger:, watch: nil, registry: nil) ⇒ ClaudeRunner

Returns a new instance of ClaudeRunner.



59
60
61
62
63
64
# File 'lib/ocak/claude_runner.rb', line 59

def initialize(config:, logger:, watch: nil, registry: nil)
  @config = config
  @logger = logger
  @watch = watch
  @registry = registry
end

Instance Method Details

#run_agent(agent_name, prompt, chdir: nil, model: nil, retries: MAX_RETRIES) ⇒ Object



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

def run_agent(agent_name, prompt, chdir: nil, model: nil, retries: MAX_RETRIES)
  chdir ||= @config.project_dir
  agent_file = @config.agent_path(agent_name)

  unless File.exist?(agent_file)
    @logger.error("Agent file not found: #{agent_file}", agent: agent_name)
    return AgentResult.new(success: false, output: "Agent file not found: #{agent_file}")
  end

  instructions = File.read(agent_file)
  full_prompt = "#{instructions}\n\n---\n\nTask: #{prompt}"
  allowed_tools = AGENT_TOOLS.fetch(agent_name, 'Read,Glob,Grep,Bash')
  agent_model = model || AGENT_MODELS[agent_name]

  run_with_retry(agent_name, full_prompt, allowed_tools, agent_model, chdir: chdir, retries: retries)
end

#run_prompt(prompt, allowed_tools: 'Read,Glob,Grep,Bash', chdir: nil, model: nil) ⇒ Object

Run a raw prompt without agent file (for planner, init analysis, etc.)



84
85
86
87
88
89
90
91
92
93
94
# File 'lib/ocak/claude_runner.rb', line 84

def run_prompt(prompt, allowed_tools: 'Read,Glob,Grep,Bash', chdir: nil, model: nil)
  chdir ||= @config.project_dir

  stdout, _, status = run_claude(prompt, allowed_tools, chdir: chdir, model: model)

  # Try to extract result from stream-json, fall back to raw stdout
  result_text = extract_result_from_stream(stdout) || stdout
  success = status.respond_to?(:success?) && status.success?

  AgentResult.new(success: success, output: result_text)
end