ocak 0.4.0 → 0.6.0
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 +4 -4
- data/README.md +101 -21
- data/lib/ocak/agent_generator.rb +11 -1
- data/lib/ocak/batch_processing.rb +132 -0
- data/lib/ocak/claude_runner.rb +12 -8
- data/lib/ocak/cli.rb +13 -0
- data/lib/ocak/command_runner.rb +39 -0
- data/lib/ocak/commands/hiz.rb +28 -28
- data/lib/ocak/commands/init.rb +37 -0
- data/lib/ocak/commands/issue/close.rb +37 -0
- data/lib/ocak/commands/issue/create.rb +59 -0
- data/lib/ocak/commands/issue/edit.rb +31 -0
- data/lib/ocak/commands/issue/list.rb +43 -0
- data/lib/ocak/commands/issue/view.rb +58 -0
- data/lib/ocak/commands/resume.rb +11 -9
- data/lib/ocak/commands/status.rb +29 -12
- data/lib/ocak/config.rb +72 -1
- data/lib/ocak/conflict_resolution.rb +73 -0
- data/lib/ocak/failure_reporting.rb +6 -3
- data/lib/ocak/git_utils.rb +18 -11
- data/lib/ocak/instance_builders.rb +54 -0
- data/lib/ocak/issue_backend.rb +31 -0
- data/lib/ocak/issue_fetcher.rb +9 -0
- data/lib/ocak/issue_state_machine.rb +36 -0
- data/lib/ocak/local_issue_fetcher.rb +165 -0
- data/lib/ocak/local_merge_manager.rb +104 -0
- data/lib/ocak/merge_manager.rb +30 -103
- data/lib/ocak/merge_orchestration.rb +36 -24
- data/lib/ocak/merge_verification.rb +40 -0
- data/lib/ocak/parallel_execution.rb +36 -0
- data/lib/ocak/pipeline_executor.rb +17 -185
- data/lib/ocak/pipeline_runner.rb +32 -180
- data/lib/ocak/planner.rb +16 -1
- data/lib/ocak/project_key.rb +38 -0
- data/lib/ocak/reready_processor.rb +11 -11
- data/lib/ocak/run_report.rb +5 -2
- data/lib/ocak/shutdown_handling.rb +67 -0
- data/lib/ocak/state_management.rb +104 -0
- data/lib/ocak/step_execution.rb +66 -0
- data/lib/ocak/stream_parser.rb +1 -1
- data/lib/ocak/target_resolver.rb +41 -0
- data/lib/ocak/templates/agents/auditor.md.erb +38 -9
- data/lib/ocak/templates/agents/implementer.md.erb +35 -8
- data/lib/ocak/templates/agents/merger.md.erb +24 -5
- data/lib/ocak/templates/agents/pipeline.md.erb +22 -0
- data/lib/ocak/templates/agents/reviewer.md.erb +2 -2
- data/lib/ocak/templates/agents/security_reviewer.md.erb +11 -0
- data/lib/ocak/templates/gitignore_additions.txt +1 -0
- data/lib/ocak/templates/ocak.yml.erb +24 -0
- data/lib/ocak/verification.rb +6 -1
- data/lib/ocak/worktree_manager.rb +9 -6
- data/lib/ocak.rb +1 -1
- metadata +21 -1
|
@@ -14,50 +14,59 @@ module Ocak
|
|
|
14
14
|
elsif @config.manual_review
|
|
15
15
|
handle_batch_manual_review(result, merger: merger, issues: issues, logger: logger)
|
|
16
16
|
elsif merger.merge(result[:issue_number], result[:worktree])
|
|
17
|
-
|
|
17
|
+
@state_machine.mark_completed(result[:issue_number])
|
|
18
18
|
logger.info("Issue ##{result[:issue_number]} merged successfully")
|
|
19
19
|
else
|
|
20
|
-
|
|
20
|
+
@state_machine.mark_failed(result[:issue_number])
|
|
21
21
|
logger.error("Issue ##{result[:issue_number]} merge failed")
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def handle_single_success(issue_number, result, logger:, claude:, issues:)
|
|
26
|
+
target_dir = result[:target_repo]&.dig(:path) || @config.project_dir
|
|
27
|
+
|
|
26
28
|
if result[:audit_blocked]
|
|
27
|
-
handle_single_audit_blocked(issue_number, result, logger: logger, claude: claude, issues: issues
|
|
29
|
+
handle_single_audit_blocked(issue_number, result, logger: logger, claude: claude, issues: issues,
|
|
30
|
+
chdir: target_dir)
|
|
28
31
|
elsif @config.manual_review
|
|
29
|
-
handle_single_manual_review(issue_number, logger: logger, claude: claude, issues: issues)
|
|
32
|
+
handle_single_manual_review(issue_number, logger: logger, claude: claude, issues: issues, chdir: target_dir)
|
|
30
33
|
else
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
unless pipeline_has_merge_step?
|
|
35
|
+
prompt = if result[:target_repo]
|
|
36
|
+
"Create a PR and merge it for issue ##{issue_number}. " \
|
|
37
|
+
'Do NOT close any issues (the issue lives in a different repository).'
|
|
38
|
+
else
|
|
39
|
+
"Create a PR, merge it, and close issue ##{issue_number}"
|
|
40
|
+
end
|
|
41
|
+
claude.run_agent('merger', prompt, chdir: target_dir)
|
|
42
|
+
end
|
|
43
|
+
@state_machine.mark_completed(issue_number)
|
|
34
44
|
logger.info("Issue ##{issue_number} completed successfully")
|
|
35
45
|
end
|
|
36
46
|
end
|
|
37
47
|
|
|
38
|
-
def handle_single_manual_review(issue_number, logger:, claude:, issues:)
|
|
48
|
+
def handle_single_manual_review(issue_number, logger:, claude:, issues: nil, chdir: @config.project_dir) # rubocop:disable Lint/UnusedMethodArgument
|
|
39
49
|
claude.run_agent('merger',
|
|
40
50
|
"Create a PR for issue ##{issue_number} but do NOT merge it and do NOT close the issue",
|
|
41
|
-
chdir:
|
|
42
|
-
|
|
51
|
+
chdir: chdir)
|
|
52
|
+
@state_machine.mark_for_review(issue_number)
|
|
43
53
|
logger.info("Issue ##{issue_number} PR created (manual review mode)")
|
|
44
54
|
end
|
|
45
55
|
|
|
46
|
-
def handle_batch_manual_review(result, merger:,
|
|
56
|
+
def handle_batch_manual_review(result, merger:, logger:, issues: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
47
57
|
pr_number = merger.create_pr_only(result[:issue_number], result[:worktree])
|
|
48
58
|
if pr_number
|
|
49
|
-
|
|
50
|
-
to: @config.label_awaiting_review)
|
|
59
|
+
@state_machine.mark_for_review(result[:issue_number])
|
|
51
60
|
logger.info("Issue ##{result[:issue_number]} PR ##{pr_number} created (manual review mode)")
|
|
52
61
|
else
|
|
53
|
-
|
|
62
|
+
@state_machine.mark_failed(result[:issue_number])
|
|
54
63
|
logger.error("Issue ##{result[:issue_number]} PR creation failed")
|
|
55
64
|
end
|
|
56
65
|
end
|
|
57
66
|
|
|
58
|
-
def handle_single_audit_blocked(issue_number, result, logger:, claude:, issues:)
|
|
59
|
-
handle_single_manual_review(issue_number, logger: logger, claude: claude, issues: issues)
|
|
60
|
-
post_audit_comment_single(result[:audit_output], logger: logger, issues: issues)
|
|
67
|
+
def handle_single_audit_blocked(issue_number, result, logger:, claude:, issues:, chdir: @config.project_dir)
|
|
68
|
+
handle_single_manual_review(issue_number, logger: logger, claude: claude, issues: issues, chdir: chdir)
|
|
69
|
+
post_audit_comment_single(result[:audit_output], logger: logger, issues: issues, chdir: chdir)
|
|
61
70
|
end
|
|
62
71
|
|
|
63
72
|
def handle_batch_audit(result, merger:, issues:, logger:)
|
|
@@ -68,17 +77,16 @@ module Ocak
|
|
|
68
77
|
pr_number = merger.create_pr_only(result[:issue_number], result[:worktree])
|
|
69
78
|
if pr_number
|
|
70
79
|
issues.pr_comment(pr_number, "## Audit Report\n\n#{audit_output}")
|
|
71
|
-
|
|
72
|
-
to: @config.label_awaiting_review)
|
|
80
|
+
@state_machine.mark_for_review(result[:issue_number])
|
|
73
81
|
logger.info("Issue ##{result[:issue_number]} PR ##{pr_number} created (audit findings)")
|
|
74
82
|
else
|
|
75
|
-
|
|
83
|
+
@state_machine.mark_failed(result[:issue_number])
|
|
76
84
|
logger.error("Issue ##{result[:issue_number]} PR creation failed")
|
|
77
85
|
end
|
|
78
86
|
end
|
|
79
87
|
|
|
80
|
-
def post_audit_comment_single(audit_output, logger:, issues:)
|
|
81
|
-
pr_number = find_pr_for_branch(logger: logger)
|
|
88
|
+
def post_audit_comment_single(audit_output, logger:, issues:, chdir: @config.project_dir)
|
|
89
|
+
pr_number = find_pr_for_branch(logger: logger, chdir: chdir)
|
|
82
90
|
unless pr_number
|
|
83
91
|
logger.warn("Could not find PR to post audit comment — findings were: #{audit_output.to_s[0..200]}")
|
|
84
92
|
return
|
|
@@ -88,8 +96,12 @@ module Ocak
|
|
|
88
96
|
logger.info("Posted audit comment on PR ##{pr_number}")
|
|
89
97
|
end
|
|
90
98
|
|
|
91
|
-
def
|
|
92
|
-
|
|
99
|
+
def pipeline_has_merge_step?
|
|
100
|
+
@config.steps.any? { |s| s[:role].to_s == 'merge' || s['role'].to_s == 'merge' }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def find_pr_for_branch(logger:, chdir: @config.project_dir)
|
|
104
|
+
stdout, _, status = Open3.capture3('gh', 'pr', 'view', '--json', 'number', chdir: chdir)
|
|
93
105
|
return nil unless status.success?
|
|
94
106
|
|
|
95
107
|
data = JSON.parse(stdout)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
|
|
6
|
+
module Ocak
|
|
7
|
+
# Test verification and git push logic — verify_tests, push_branch.
|
|
8
|
+
# Extracted from MergeManager to reduce file size.
|
|
9
|
+
module MergeVerification
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def verify_tests(worktree)
|
|
13
|
+
test_cmd = @config.test_command
|
|
14
|
+
return true unless test_cmd
|
|
15
|
+
|
|
16
|
+
@logger.info('Running tests after rebase...')
|
|
17
|
+
stdout, stderr, status = shell(test_cmd, chdir: worktree.path)
|
|
18
|
+
|
|
19
|
+
if status.success?
|
|
20
|
+
@logger.info('Tests passed after rebase')
|
|
21
|
+
true
|
|
22
|
+
else
|
|
23
|
+
@logger.warn('Tests failed after rebase')
|
|
24
|
+
@logger.debug("Test output:\n#{stdout[0..2000]}\n#{stderr[0..500]}")
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def push_branch(worktree)
|
|
30
|
+
result = run_git('push', '-u', 'origin', worktree.branch, chdir: worktree.path)
|
|
31
|
+
|
|
32
|
+
unless result.success?
|
|
33
|
+
@logger.error("Push failed: #{result.error}")
|
|
34
|
+
return false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ocak
|
|
4
|
+
# Parallel group execution logic extracted from PipelineExecutor.
|
|
5
|
+
# Includers must provide run_single_step method and symbolize helper.
|
|
6
|
+
module ParallelExecution
|
|
7
|
+
def collect_parallel_group(steps, start_idx)
|
|
8
|
+
group = []
|
|
9
|
+
idx = start_idx
|
|
10
|
+
while idx < steps.size
|
|
11
|
+
step = symbolize(steps[idx])
|
|
12
|
+
break unless step[:parallel]
|
|
13
|
+
|
|
14
|
+
group << [step, idx]
|
|
15
|
+
idx += 1
|
|
16
|
+
end
|
|
17
|
+
group
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run_parallel_group(group, issue_number, state, logger:, claude:, chdir:)
|
|
21
|
+
mutex = Mutex.new
|
|
22
|
+
threads = group.map do |step, idx|
|
|
23
|
+
Thread.new do
|
|
24
|
+
run_single_step(step, idx, issue_number, state, logger: logger, claude: claude,
|
|
25
|
+
chdir: chdir, mutex: mutex)
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
logger.error("#{step[:role]} thread failed: #{e.message}")
|
|
28
|
+
{ success: false, phase: step[:role].to_s, output: "Thread error: #{e.message}" }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
results = threads.map(&:value)
|
|
33
|
+
results.compact.find { |r| r.is_a?(Hash) && !r[:success] }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -2,19 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
require 'open3'
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require_relative 'command_runner'
|
|
5
6
|
require_relative 'pipeline_state'
|
|
6
7
|
require_relative 'run_report'
|
|
7
8
|
require_relative 'verification'
|
|
8
9
|
require_relative 'planner'
|
|
9
10
|
require_relative 'step_comments'
|
|
11
|
+
require_relative 'state_management'
|
|
12
|
+
require_relative 'step_execution'
|
|
13
|
+
require_relative 'parallel_execution'
|
|
10
14
|
|
|
11
15
|
module Ocak
|
|
12
16
|
class PipelineExecutor
|
|
17
|
+
include CommandRunner
|
|
13
18
|
include Verification
|
|
14
19
|
include Planner
|
|
15
20
|
include StepComments
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
include StateManagement
|
|
22
|
+
include StepExecution
|
|
23
|
+
include ParallelExecution
|
|
18
24
|
|
|
19
25
|
attr_writer :issues
|
|
20
26
|
|
|
@@ -26,11 +32,13 @@ module Ocak
|
|
|
26
32
|
|
|
27
33
|
def run_pipeline(issue_number, logger:, claude:, chdir: nil, skip_steps: [], complexity: 'full', # rubocop:disable Metrics/ParameterLists
|
|
28
34
|
steps: nil, verification_model: nil,
|
|
29
|
-
post_start_comment: true, post_summary_comment: true
|
|
35
|
+
post_start_comment: true, post_summary_comment: true,
|
|
36
|
+
skip_merge: false)
|
|
30
37
|
@logger = logger
|
|
31
38
|
@custom_steps = steps
|
|
32
39
|
@verification_model = verification_model
|
|
33
40
|
@post_summary_comment = post_summary_comment
|
|
41
|
+
@skip_merge = skip_merge
|
|
34
42
|
chdir ||= @config.project_dir
|
|
35
43
|
logger.info("=== Starting pipeline for issue ##{issue_number} (#{complexity}) ===")
|
|
36
44
|
|
|
@@ -120,55 +128,6 @@ module Ocak
|
|
|
120
128
|
nil
|
|
121
129
|
end
|
|
122
130
|
|
|
123
|
-
def collect_parallel_group(steps, start_idx)
|
|
124
|
-
group = []
|
|
125
|
-
idx = start_idx
|
|
126
|
-
while idx < steps.size
|
|
127
|
-
step = symbolize(steps[idx])
|
|
128
|
-
break unless step[:parallel]
|
|
129
|
-
|
|
130
|
-
group << [step, idx]
|
|
131
|
-
idx += 1
|
|
132
|
-
end
|
|
133
|
-
group
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def run_parallel_group(group, issue_number, state, logger:, claude:, chdir:)
|
|
137
|
-
mutex = Mutex.new
|
|
138
|
-
threads = group.map do |step, idx|
|
|
139
|
-
Thread.new do
|
|
140
|
-
run_single_step(step, idx, issue_number, state, logger: logger, claude: claude,
|
|
141
|
-
chdir: chdir, mutex: mutex)
|
|
142
|
-
rescue StandardError => e
|
|
143
|
-
logger.error("#{step[:role]} thread failed: #{e.message}")
|
|
144
|
-
nil
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
results = threads.map(&:value)
|
|
149
|
-
results.compact.find { |r| r.is_a?(Hash) && !r[:success] }
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def run_single_step(step, idx, issue_number, state, logger:, claude:, chdir:, mutex: nil) # rubocop:disable Metrics/ParameterLists
|
|
153
|
-
role = step[:role].to_s
|
|
154
|
-
agent = step[:agent].to_s
|
|
155
|
-
|
|
156
|
-
return nil if handle_already_completed(idx, role, @skip_steps, logger)
|
|
157
|
-
|
|
158
|
-
reason = skip_reason(step, state)
|
|
159
|
-
if reason
|
|
160
|
-
logger.info("Skipping #{role} \u2014 #{reason}")
|
|
161
|
-
record_skipped_step(issue_number, state, idx, agent, role, reason)
|
|
162
|
-
return nil
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
result = execute_step(step, issue_number, state[:last_review_output], logger: logger, claude: claude,
|
|
166
|
-
chdir: chdir)
|
|
167
|
-
state[:report].record_step(index: idx, agent: agent, role: role, status: 'completed', result: result)
|
|
168
|
-
ctx = StepContext.new(issue_number, idx, role, result, state, logger, chdir)
|
|
169
|
-
record_step_result(ctx, mutex: mutex)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
131
|
def check_shutdown(state, logger)
|
|
173
132
|
return false unless @shutdown_check&.call
|
|
174
133
|
|
|
@@ -176,121 +135,6 @@ module Ocak
|
|
|
176
135
|
state[:interrupted] = true
|
|
177
136
|
end
|
|
178
137
|
|
|
179
|
-
def handle_already_completed(idx, role, skip_steps, logger)
|
|
180
|
-
return false unless skip_steps.include?(idx)
|
|
181
|
-
|
|
182
|
-
logger.info("Skipping #{role} (already completed)")
|
|
183
|
-
true
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def record_skipped_step(issue_number, state, idx, agent, role, reason)
|
|
187
|
-
post_step_comment(issue_number, "\u{23ED}\u{FE0F} **Skipping #{role}** \u2014 #{reason}")
|
|
188
|
-
state[:report].record_step(index: idx, agent: agent, role: role, status: 'skipped', skip_reason: reason)
|
|
189
|
-
state[:steps_skipped] += 1
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def execute_step(step, issue_number, review_output, logger:, claude:, chdir:)
|
|
193
|
-
agent = step[:agent].to_s
|
|
194
|
-
role = step[:role].to_s
|
|
195
|
-
logger.info("--- Phase: #{role} (#{agent}) ---")
|
|
196
|
-
post_step_comment(issue_number, "\u{1F504} **Phase: #{role}** (#{agent})")
|
|
197
|
-
prompt = build_step_prompt(role, issue_number, review_output)
|
|
198
|
-
opts = { chdir: chdir }
|
|
199
|
-
opts[:model] = step[:model].to_s if step[:model]
|
|
200
|
-
claude.run_agent(agent.tr('_', '-'), prompt, **opts)
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def record_step_result(ctx, mutex: nil)
|
|
204
|
-
sync(mutex) { accumulate_state(ctx) }
|
|
205
|
-
save_step_progress(ctx)
|
|
206
|
-
write_step_output(ctx.issue_number, ctx.idx, ctx.role, ctx.result.output)
|
|
207
|
-
post_step_completion_comment(ctx.issue_number, ctx.role, ctx.result)
|
|
208
|
-
|
|
209
|
-
check_step_failure(ctx) || check_cost_budget(ctx.state, ctx.logger)
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def accumulate_state(ctx)
|
|
213
|
-
update_pipeline_state(ctx.role, ctx.result, ctx.state)
|
|
214
|
-
ctx.state[:completed_steps] << ctx.idx
|
|
215
|
-
ctx.state[:steps_run] += 1
|
|
216
|
-
ctx.state[:total_cost] += ctx.result.cost_usd.to_f
|
|
217
|
-
ctx.state[:step_results][ctx.role] = ctx.result
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def sync(mutex, &)
|
|
221
|
-
if mutex
|
|
222
|
-
mutex.synchronize(&)
|
|
223
|
-
else
|
|
224
|
-
yield
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def save_step_progress(ctx)
|
|
229
|
-
pipeline_state.save(ctx.issue_number,
|
|
230
|
-
completed_steps: ctx.state[:completed_steps],
|
|
231
|
-
worktree_path: ctx.chdir,
|
|
232
|
-
branch: current_branch(ctx.chdir, logger: ctx.logger))
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def write_step_output(issue_number, idx, agent, output)
|
|
236
|
-
return if output.to_s.empty?
|
|
237
|
-
return unless issue_number.to_s.match?(/\A\d+\z/)
|
|
238
|
-
|
|
239
|
-
safe_agent = agent.to_s.gsub(/[^a-zA-Z0-9_-]/, '')
|
|
240
|
-
dir = File.join(@config.project_dir, '.ocak', 'logs', "issue-#{issue_number}")
|
|
241
|
-
FileUtils.mkdir_p(dir)
|
|
242
|
-
File.write(File.join(dir, "step-#{idx}-#{safe_agent}.md"), output)
|
|
243
|
-
rescue StandardError => e
|
|
244
|
-
@logger&.debug("Step output write failed: #{e.message}")
|
|
245
|
-
nil
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def check_step_failure(ctx)
|
|
249
|
-
return nil if ctx.result.success? || !%w[implement merge].include?(ctx.role)
|
|
250
|
-
|
|
251
|
-
ctx.logger.error("#{ctx.role} failed")
|
|
252
|
-
{ success: false, phase: ctx.role, output: ctx.result.output }
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
def check_cost_budget(state, logger)
|
|
256
|
-
return nil unless @config.cost_budget && state[:total_cost] > @config.cost_budget
|
|
257
|
-
|
|
258
|
-
cost = format('%.2f', state[:total_cost])
|
|
259
|
-
budget = format('%.2f', @config.cost_budget)
|
|
260
|
-
logger.error("Cost budget exceeded ($#{cost}/$#{budget})")
|
|
261
|
-
{ success: false, phase: 'budget', output: "Cost budget exceeded: $#{cost}" }
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def skip_reason(step, state)
|
|
265
|
-
condition = step[:condition]
|
|
266
|
-
|
|
267
|
-
return 'audit found blocking issues' if step[:role].to_s == 'merge' && @config.audit_mode && state[:audit_blocked]
|
|
268
|
-
return 'manual review mode' if step[:role].to_s == 'merge' && @config.manual_review
|
|
269
|
-
return 'fast-track issue (simple complexity)' if step[:complexity] == 'full' && state[:complexity] == 'simple'
|
|
270
|
-
if condition == 'has_findings' && !state[:last_review_output]&.include?("\u{1F534}")
|
|
271
|
-
return 'no blocking findings from review'
|
|
272
|
-
end
|
|
273
|
-
return 'no fixes were made' if condition == 'had_fixes' && !state[:had_fixes]
|
|
274
|
-
|
|
275
|
-
nil
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
def update_pipeline_state(role, result, state)
|
|
279
|
-
case role
|
|
280
|
-
when 'review', 'verify', 'security', 'audit'
|
|
281
|
-
state[:last_review_output] = result.output
|
|
282
|
-
if role == 'audit'
|
|
283
|
-
state[:audit_output] = result.output
|
|
284
|
-
state[:audit_blocked] = !result.success? || result.output.to_s.match?(/BLOCK|🔴/)
|
|
285
|
-
end
|
|
286
|
-
when 'fix'
|
|
287
|
-
state[:had_fixes] = true
|
|
288
|
-
state[:last_review_output] = nil
|
|
289
|
-
when 'implement'
|
|
290
|
-
state[:last_review_output] = nil
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
|
|
294
138
|
def run_final_verification(issue_number, logger:, claude:, chdir:)
|
|
295
139
|
run_verification_with_retry(logger: logger, claude: claude, chdir: chdir,
|
|
296
140
|
model: @verification_model) do |body|
|
|
@@ -298,29 +142,17 @@ module Ocak
|
|
|
298
142
|
end
|
|
299
143
|
end
|
|
300
144
|
|
|
301
|
-
def log_cost_summary(total_cost, logger)
|
|
302
|
-
return if total_cost.zero?
|
|
303
|
-
|
|
304
|
-
budget = @config.cost_budget
|
|
305
|
-
budget_str = budget ? " / $#{format('%.2f', budget)} budget" : ''
|
|
306
|
-
logger.info("Pipeline cost: $#{format('%.4f', total_cost)}#{budget_str}")
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def save_report(report, issue_number, success:, failed_phase: nil)
|
|
310
|
-
report.finish(success: success, failed_phase: failed_phase)
|
|
311
|
-
report.save(issue_number, project_dir: @config.project_dir)
|
|
312
|
-
rescue StandardError => e
|
|
313
|
-
@logger&.debug("Report save failed: #{e.message}")
|
|
314
|
-
nil
|
|
315
|
-
end
|
|
316
|
-
|
|
317
145
|
def pipeline_state
|
|
318
146
|
@pipeline_state ||= PipelineState.new(log_dir: File.join(@config.project_dir, @config.log_dir))
|
|
319
147
|
end
|
|
320
148
|
|
|
321
149
|
def current_branch(chdir, logger: nil)
|
|
322
|
-
|
|
323
|
-
|
|
150
|
+
result = run_git('rev-parse', '--abbrev-ref', 'HEAD', chdir: chdir)
|
|
151
|
+
if result.status.nil?
|
|
152
|
+
logger&.debug("Could not determine current branch: #{result.error}")
|
|
153
|
+
return nil
|
|
154
|
+
end
|
|
155
|
+
result.output
|
|
324
156
|
rescue StandardError => e
|
|
325
157
|
logger&.debug("Could not determine current branch: #{e.message}")
|
|
326
158
|
nil
|