Module: Ocak::StateManagement

Included in:
PipelineExecutor
Defined in:
lib/ocak/state_management.rb

Overview

State accumulation and reporting logic extracted from PipelineExecutor. Includers must provide @config, @logger instance variables and pipeline_state, current_branch methods.

Defined Under Namespace

Classes: StepContext

Instance Method Summary collapse

Instance Method Details

#accumulate_state(ctx) ⇒ Object



20
21
22
23
24
25
26
# File 'lib/ocak/state_management.rb', line 20

def accumulate_state(ctx)
  update_pipeline_state(ctx.role, ctx.result, ctx.state)
  ctx.state[:completed_steps] << ctx.idx
  ctx.state[:steps_run] += 1
  ctx.state[:total_cost] += ctx.result.cost_usd.to_f
  ctx.state[:step_results][ctx.role] = ctx.result
end

#check_cost_budget(state, logger) ⇒ Object



63
64
65
66
67
68
69
70
# File 'lib/ocak/state_management.rb', line 63

def check_cost_budget(state, logger)
  return nil unless @config.cost_budget && state[:total_cost] > @config.cost_budget

  cost = format('%.2f', state[:total_cost])
  budget = format('%.2f', @config.cost_budget)
  logger.error("Cost budget exceeded ($#{cost}/$#{budget})")
  { success: false, phase: 'budget', output: "Cost budget exceeded: $#{cost}" }
end

#check_step_failure(ctx) ⇒ Object



56
57
58
59
60
61
# File 'lib/ocak/state_management.rb', line 56

def check_step_failure(ctx)
  return nil if ctx.result.success? || !%w[implement merge].include?(ctx.role)

  ctx.logger.error("#{ctx.role} failed")
  { success: false, phase: ctx.role, output: ctx.result.output }
end

#log_cost_summary(total_cost, logger) ⇒ Object



88
89
90
91
92
93
94
# File 'lib/ocak/state_management.rb', line 88

def log_cost_summary(total_cost, logger)
  return if total_cost.zero?

  budget = @config.cost_budget
  budget_str = budget ? " / $#{format('%.2f', budget)} budget" : ''
  logger.info("Pipeline cost: $#{format('%.4f', total_cost)}#{budget_str}")
end

#record_step_result(ctx, mutex: nil) ⇒ Object



11
12
13
14
15
16
17
18
# File 'lib/ocak/state_management.rb', line 11

def record_step_result(ctx, mutex: nil)
  sync(mutex) { accumulate_state(ctx) }
  save_step_progress(ctx)
  write_step_output(ctx.issue_number, ctx.idx, ctx.role, ctx.result.output)
  post_step_completion_comment(ctx.issue_number, ctx.role, ctx.result)

  check_step_failure(ctx) || check_cost_budget(ctx.state, ctx.logger)
end

#save_report(report, issue_number, success:, failed_phase: nil) ⇒ Object



96
97
98
99
100
101
102
# File 'lib/ocak/state_management.rb', line 96

def save_report(report, issue_number, success:, failed_phase: nil)
  report.finish(success: success, failed_phase: failed_phase)
  report.save(issue_number, project_dir: @config.project_dir)
rescue StandardError => e
  @logger&.debug("Report save failed: #{e.message}")
  nil
end

#save_step_progress(ctx) ⇒ Object



36
37
38
39
40
41
# File 'lib/ocak/state_management.rb', line 36

def save_step_progress(ctx)
  pipeline_state.save(ctx.issue_number,
                      completed_steps: ctx.state[:completed_steps],
                      worktree_path: ctx.chdir,
                      branch: current_branch(ctx.chdir, logger: ctx.logger))
end

#sync(mutex) ⇒ Object



28
29
30
31
32
33
34
# File 'lib/ocak/state_management.rb', line 28

def sync(mutex, &)
  if mutex
    mutex.synchronize(&)
  else
    yield
  end
end

#update_pipeline_state(role, result, state) ⇒ Object



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/ocak/state_management.rb', line 72

def update_pipeline_state(role, result, state)
  case role
  when 'review', 'verify', 'security', 'audit'
    state[:last_review_output] = result.output
    if role == 'audit'
      state[:audit_output] = result.output
      state[:audit_blocked] = !result.success? || result.output.to_s.match?(/BLOCK|🔴/)
    end
  when 'fix'
    state[:had_fixes] = true
    state[:last_review_output] = nil
  when 'implement'
    state[:last_review_output] = nil
  end
end

#write_step_output(issue_number, idx, agent, output) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/ocak/state_management.rb', line 43

def write_step_output(issue_number, idx, agent, output)
  return if output.to_s.empty?
  return unless issue_number.to_s.match?(/\A\d+\z/)

  safe_agent = agent.to_s.gsub(/[^a-zA-Z0-9_-]/, '')
  dir = File.join(@config.project_dir, '.ocak', 'logs', "issue-#{issue_number}")
  FileUtils.mkdir_p(dir)
  File.write(File.join(dir, "step-#{idx}-#{safe_agent}.md"), output)
rescue StandardError => e
  @logger&.debug("Step output write failed: #{e.message}")
  nil
end