ocak 0.5.0 → 0.6.1
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 +75 -22
- data/lib/ocak/agent_generator.rb +9 -0
- data/lib/ocak/batch_processing.rb +35 -5
- data/lib/ocak/commands/hiz.rb +20 -21
- data/lib/ocak/commands/init.rb +32 -0
- data/lib/ocak/commands/resume.rb +7 -6
- data/lib/ocak/commands/status.rb +9 -12
- data/lib/ocak/config.rb +68 -1
- data/lib/ocak/conflict_resolution.rb +73 -0
- data/lib/ocak/failure_reporting.rb +3 -1
- data/lib/ocak/instance_builders.rb +5 -1
- data/lib/ocak/issue_fetcher.rb +9 -0
- data/lib/ocak/issue_state_machine.rb +36 -0
- data/lib/ocak/merge_manager.rb +25 -94
- data/lib/ocak/merge_orchestration.rb +40 -26
- data/lib/ocak/merge_verification.rb +40 -0
- data/lib/ocak/pipeline_executor.rb +9 -2
- data/lib/ocak/pipeline_runner.rb +18 -1
- data/lib/ocak/planner.rb +36 -8
- data/lib/ocak/shutdown_handling.rb +2 -2
- data/lib/ocak/step_execution.rb +14 -6
- data/lib/ocak/stream_parser.rb +1 -1
- data/lib/ocak/target_resolver.rb +41 -0
- data/lib/ocak/templates/agents/implementer.md.erb +4 -1
- data/lib/ocak/templates/agents/merger.md.erb +14 -3
- data/lib/ocak/templates/agents/pipeline.md.erb +18 -0
- data/lib/ocak/templates/gitignore_additions.txt +1 -0
- data/lib/ocak/templates/ocak.yml.erb +9 -0
- data/lib/ocak/worktree_manager.rb +7 -6
- data/lib/ocak.rb +1 -1
- metadata +5 -1
data/lib/ocak/step_execution.rb
CHANGED
|
@@ -18,7 +18,8 @@ module Ocak
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
result = execute_step(step, issue_number, state[:last_review_output], logger: logger, claude: claude,
|
|
21
|
-
chdir: chdir
|
|
21
|
+
chdir: chdir,
|
|
22
|
+
issue_data: state[:issue_data])
|
|
22
23
|
state[:report].record_step(index: idx, agent: agent, role: role, status: 'completed', result: result)
|
|
23
24
|
ctx = StateManagement::StepContext.new(issue_number, idx, role, result, state, logger, chdir)
|
|
24
25
|
record_step_result(ctx, mutex: mutex)
|
|
@@ -37,12 +38,12 @@ module Ocak
|
|
|
37
38
|
state[:steps_skipped] += 1
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
def execute_step(step, issue_number, review_output, logger:, claude:, chdir:)
|
|
41
|
+
def execute_step(step, issue_number, review_output, logger:, claude:, chdir:, issue_data: nil) # rubocop:disable Metrics/ParameterLists
|
|
41
42
|
agent = step[:agent].to_s
|
|
42
43
|
role = step[:role].to_s
|
|
43
44
|
logger.info("--- Phase: #{role} (#{agent}) ---")
|
|
44
45
|
post_step_comment(issue_number, "🔄 **Phase: #{role}** (#{agent})")
|
|
45
|
-
prompt = build_step_prompt(role, issue_number, review_output)
|
|
46
|
+
prompt = build_step_prompt(role, issue_number, review_output, issue_data: issue_data)
|
|
46
47
|
opts = { chdir: chdir }
|
|
47
48
|
opts[:model] = step[:model].to_s if step[:model]
|
|
48
49
|
claude.run_agent(agent.tr('_', '-'), prompt, **opts)
|
|
@@ -51,9 +52,8 @@ module Ocak
|
|
|
51
52
|
def skip_reason(step, state)
|
|
52
53
|
condition = step[:condition]
|
|
53
54
|
|
|
54
|
-
return
|
|
55
|
-
return 'audit
|
|
56
|
-
return 'manual review mode' if step[:role].to_s == 'merge' && @config.manual_review
|
|
55
|
+
return merge_skip_reason(state) if step[:role].to_s == 'merge'
|
|
56
|
+
return 'audit mode not enabled' if step[:role].to_s == 'audit' && !@config.audit_mode
|
|
57
57
|
return 'fast-track issue (simple complexity)' if step[:complexity] == 'full' && state[:complexity] == 'simple'
|
|
58
58
|
if condition == 'has_findings' && !state[:last_review_output]&.include?('🔴')
|
|
59
59
|
return 'no blocking findings from review'
|
|
@@ -62,5 +62,13 @@ module Ocak
|
|
|
62
62
|
|
|
63
63
|
nil
|
|
64
64
|
end
|
|
65
|
+
|
|
66
|
+
def merge_skip_reason(state)
|
|
67
|
+
return 'merge handled by MergeManager' if @skip_merge
|
|
68
|
+
return 'audit found blocking issues' if @config.audit_mode && state[:audit_blocked]
|
|
69
|
+
return 'manual review mode' if @config.manual_review
|
|
70
|
+
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
65
73
|
end
|
|
66
74
|
end
|
data/lib/ocak/stream_parser.rb
CHANGED
|
@@ -191,7 +191,7 @@ module Ocak
|
|
|
191
191
|
end
|
|
192
192
|
|
|
193
193
|
def detect_test_pass(output)
|
|
194
|
-
return true if output.match?(
|
|
194
|
+
return true if output.match?(/\b0 failures\b/)
|
|
195
195
|
return true if output.match?(/no offenses detected/i)
|
|
196
196
|
return true if output.match?(/test result: ok/i) # cargo test
|
|
197
197
|
return false if output.match?(/[1-9]\d* failures?/) || output.match?(/[1-9]\d* errors?/)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Ocak
|
|
6
|
+
module TargetResolver
|
|
7
|
+
# Resolves the target repo for an issue.
|
|
8
|
+
# Returns { name:, path: } hash or nil (single-repo mode).
|
|
9
|
+
# Raises TargetResolutionError on missing/invalid target in multi-repo mode.
|
|
10
|
+
def self.resolve(issue, config:)
|
|
11
|
+
return nil unless config.multi_repo?
|
|
12
|
+
|
|
13
|
+
body = issue['body'].to_s
|
|
14
|
+
repo_name = extract_target_name(body, field: config.target_field)
|
|
15
|
+
|
|
16
|
+
unless repo_name
|
|
17
|
+
raise TargetResolutionError,
|
|
18
|
+
"Issue ##{issue['number']} is missing required '#{config.target_field}' field in body. " \
|
|
19
|
+
"Known repos: #{config.repos.keys.join(', ')}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
config.resolve_repo(repo_name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Extract target name from YAML front-matter.
|
|
26
|
+
# Returns the target name string, or nil if not found.
|
|
27
|
+
def self.extract_target_name(body, field:)
|
|
28
|
+
match = body.match(/\A---\s*\n(.*?)\n---/m)
|
|
29
|
+
return nil unless match
|
|
30
|
+
|
|
31
|
+
frontmatter = YAML.safe_load(match[1])
|
|
32
|
+
return nil unless frontmatter.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
frontmatter[field]&.to_s&.strip
|
|
35
|
+
rescue Psych::SyntaxError
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class TargetResolutionError < StandardError; end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -12,7 +12,7 @@ You implement GitHub issues. The issue is your single source of truth — everyt
|
|
|
12
12
|
## Before You Start
|
|
13
13
|
|
|
14
14
|
1. Check branch state: `git log main..HEAD --oneline` and `git diff main --stat` — work may already be partially done
|
|
15
|
-
2. Read the issue
|
|
15
|
+
2. Read the issue data provided in your task prompt (in `<issue_data>` tags) — this is your source of truth for the issue title and body
|
|
16
16
|
3. Read `CLAUDE.md` for project conventions
|
|
17
17
|
4. Read every file listed in the issue's "Relevant files" and "Patterns to Follow" sections
|
|
18
18
|
5. When writing new code, find the closest existing example first (neighboring file in the same directory/namespace) and mirror its structure exactly
|
|
@@ -98,6 +98,9 @@ You implement GitHub issues. The issue is your single source of truth — everyt
|
|
|
98
98
|
<%- end -%>
|
|
99
99
|
<%- end -%>
|
|
100
100
|
|
|
101
|
+
### Working Directory Context
|
|
102
|
+
If a CLAUDE.md file exists in the current working directory, read it for project-specific conventions, patterns, and architecture. This is especially important when working in a repository you haven't seen before.
|
|
103
|
+
|
|
101
104
|
### General Rules
|
|
102
105
|
|
|
103
106
|
- Do NOT add features, refactoring, or improvements beyond what the issue specifies
|
|
@@ -24,16 +24,24 @@ Only re-run tests if you had to resolve merge conflicts or edit files:
|
|
|
24
24
|
<%- if lint_command -%>
|
|
25
25
|
<%= lint_command %>
|
|
26
26
|
<%- end -%>
|
|
27
|
+
<%- unless test_command || lint_command -%>
|
|
28
|
+
# No specific test/lint commands configured. Detect from project structure:
|
|
29
|
+
# - Gemfile → bundle exec rspec (or bundle exec rake test)
|
|
30
|
+
# - package.json → npm test
|
|
31
|
+
# - Cargo.toml → cargo test
|
|
32
|
+
# - go.mod → go test ./...
|
|
33
|
+
# - pyproject.toml / setup.py → pytest
|
|
34
|
+
# Read the project's CLAUDE.md for additional conventions.
|
|
35
|
+
<%- end -%>
|
|
27
36
|
```
|
|
28
37
|
|
|
29
38
|
If anything fails after conflict resolution, STOP and report the failures. Do not create a PR with failing checks.
|
|
30
39
|
|
|
31
40
|
### 2. Create Pull Request
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
# Get the issue title and body for context
|
|
35
|
-
gh issue view <number> --json title,body
|
|
42
|
+
Use the issue data provided in your task prompt (in `<issue_data>` tags) for the issue title and body context. Do NOT run `gh issue view` — the issue may live in a different repository.
|
|
36
43
|
|
|
44
|
+
```bash
|
|
37
45
|
# Get full diff for PR description
|
|
38
46
|
git diff main --stat
|
|
39
47
|
git log main..HEAD --oneline
|
|
@@ -63,6 +71,9 @@ Closes #<issue-number>
|
|
|
63
71
|
<%- if lint_command -%>
|
|
64
72
|
- `<%= lint_command %>` — passed
|
|
65
73
|
<%- end -%>
|
|
74
|
+
<%- unless test_command || lint_command -%>
|
|
75
|
+
- Stack auto-detected from project structure; commands detected at runtime
|
|
76
|
+
<%- end -%>
|
|
66
77
|
EOF
|
|
67
78
|
)"
|
|
68
79
|
```
|
|
@@ -33,6 +33,15 @@ gh issue view <number> --json title,body,labels
|
|
|
33
33
|
<%- if test_command -%>
|
|
34
34
|
- `<%= test_command %>`
|
|
35
35
|
<%- end -%>
|
|
36
|
+
<%- unless lint_command || test_command -%>
|
|
37
|
+
- No specific commands configured. Detect from project structure:
|
|
38
|
+
- Gemfile → bundle exec rspec (or bundle exec rake test)
|
|
39
|
+
- package.json → npm test
|
|
40
|
+
- Cargo.toml → cargo test
|
|
41
|
+
- go.mod → go test ./...
|
|
42
|
+
- pyproject.toml / setup.py → pytest
|
|
43
|
+
- Read the project's CLAUDE.md for additional conventions.
|
|
44
|
+
<%- end -%>
|
|
36
45
|
|
|
37
46
|
### Phase 2: Self-Review
|
|
38
47
|
|
|
@@ -88,6 +97,15 @@ Run the complete check suite one last time:
|
|
|
88
97
|
<%- if test_command -%>
|
|
89
98
|
<%= test_command %>
|
|
90
99
|
<%- end -%>
|
|
100
|
+
<%- unless lint_command || test_command -%>
|
|
101
|
+
# No specific commands configured. Detect from project structure:
|
|
102
|
+
# - Gemfile → bundle exec rspec (or bundle exec rake test)
|
|
103
|
+
# - package.json → npm test
|
|
104
|
+
# - Cargo.toml → cargo test
|
|
105
|
+
# - go.mod → go test ./...
|
|
106
|
+
# - pyproject.toml / setup.py → pytest
|
|
107
|
+
# Read the project's CLAUDE.md for additional conventions.
|
|
108
|
+
<%- end -%>
|
|
91
109
|
```
|
|
92
110
|
|
|
93
111
|
ALL must pass. If anything fails, fix it. Maximum 3 fix cycles — if still failing after 3 attempts, stop and report what's broken.
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# WARNING: Commands configured in this file (test_command, lint_command, format_command,
|
|
2
|
+
# setup_command, security_commands) are executed with your user privileges. Treat this
|
|
3
|
+
# file like executable code — review it before running `ocak run` in an unfamiliar repo.
|
|
4
|
+
|
|
1
5
|
# Ocak pipeline configuration
|
|
2
6
|
# Generated by `ocak init` — customize as needed
|
|
3
7
|
|
|
@@ -27,6 +31,11 @@ stack:
|
|
|
27
31
|
<%- end -%>
|
|
28
32
|
<%- end -%>
|
|
29
33
|
|
|
34
|
+
# Multi-repo mode — process issues across multiple local repos
|
|
35
|
+
# Repo path mappings go in ~/.config/ocak/config.yml (machine-specific, not committed)
|
|
36
|
+
# multi_repo: true
|
|
37
|
+
# target_field: target_repo # YAML front-matter field in issue body
|
|
38
|
+
|
|
30
39
|
# Issue backend — 'github' (default) or 'local'
|
|
31
40
|
# Local mode stores issues as files in .ocak/issues/ (no gh CLI needed for issues).
|
|
32
41
|
# Auto-detected: if .ocak/issues/ contains .md files, local mode is used automatically.
|
|
@@ -7,14 +7,15 @@ require 'shellwords'
|
|
|
7
7
|
|
|
8
8
|
module Ocak
|
|
9
9
|
class WorktreeManager
|
|
10
|
-
def initialize(config:, logger: nil)
|
|
10
|
+
def initialize(config:, repo_dir: nil, logger: nil)
|
|
11
11
|
@config = config
|
|
12
12
|
@logger = logger
|
|
13
|
-
@
|
|
13
|
+
@repo_dir = repo_dir || config.project_dir
|
|
14
|
+
@worktree_base = File.join(@repo_dir, config.worktree_dir)
|
|
14
15
|
@mutex = Mutex.new
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
def create(issue_number, setup_command: nil)
|
|
18
|
+
def create(issue_number, setup_command: nil, target_repo: nil)
|
|
18
19
|
@mutex.synchronize do
|
|
19
20
|
raise ArgumentError, "Invalid issue number: #{issue_number}" unless issue_number.to_s.match?(/\A\d+\z/)
|
|
20
21
|
|
|
@@ -31,7 +32,7 @@ module Ocak
|
|
|
31
32
|
raise WorktreeError, "Setup command failed: #{stderr}" unless status.success?
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
Worktree.new(path: path, branch: branch, issue_number: issue_number)
|
|
35
|
+
Worktree.new(path: path, branch: branch, issue_number: issue_number, target_repo: target_repo)
|
|
35
36
|
end
|
|
36
37
|
end
|
|
37
38
|
|
|
@@ -68,14 +69,14 @@ module Ocak
|
|
|
68
69
|
removed
|
|
69
70
|
end
|
|
70
71
|
|
|
71
|
-
Worktree = Struct.new(:path, :branch, :issue_number, keyword_init: true) # rubocop:disable Style/RedundantStructKeywordInit
|
|
72
|
+
Worktree = Struct.new(:path, :branch, :issue_number, :target_repo, keyword_init: true) # rubocop:disable Style/RedundantStructKeywordInit
|
|
72
73
|
|
|
73
74
|
class WorktreeError < StandardError; end
|
|
74
75
|
|
|
75
76
|
private
|
|
76
77
|
|
|
77
78
|
def git(*)
|
|
78
|
-
Open3.capture3('git', *, chdir: @
|
|
79
|
+
Open3.capture3('git', *, chdir: @repo_dir)
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
def parse_worktree_list(output)
|
data/lib/ocak.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ocak
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Clay Harmon
|
|
@@ -55,16 +55,19 @@ files:
|
|
|
55
55
|
- lib/ocak/commands/run.rb
|
|
56
56
|
- lib/ocak/commands/status.rb
|
|
57
57
|
- lib/ocak/config.rb
|
|
58
|
+
- lib/ocak/conflict_resolution.rb
|
|
58
59
|
- lib/ocak/failure_reporting.rb
|
|
59
60
|
- lib/ocak/git_utils.rb
|
|
60
61
|
- lib/ocak/instance_builders.rb
|
|
61
62
|
- lib/ocak/issue_backend.rb
|
|
62
63
|
- lib/ocak/issue_fetcher.rb
|
|
64
|
+
- lib/ocak/issue_state_machine.rb
|
|
63
65
|
- lib/ocak/local_issue_fetcher.rb
|
|
64
66
|
- lib/ocak/local_merge_manager.rb
|
|
65
67
|
- lib/ocak/logger.rb
|
|
66
68
|
- lib/ocak/merge_manager.rb
|
|
67
69
|
- lib/ocak/merge_orchestration.rb
|
|
70
|
+
- lib/ocak/merge_verification.rb
|
|
68
71
|
- lib/ocak/monorepo_detector.rb
|
|
69
72
|
- lib/ocak/parallel_execution.rb
|
|
70
73
|
- lib/ocak/pipeline_executor.rb
|
|
@@ -82,6 +85,7 @@ files:
|
|
|
82
85
|
- lib/ocak/step_comments.rb
|
|
83
86
|
- lib/ocak/step_execution.rb
|
|
84
87
|
- lib/ocak/stream_parser.rb
|
|
88
|
+
- lib/ocak/target_resolver.rb
|
|
85
89
|
- lib/ocak/templates/agents/auditor.md.erb
|
|
86
90
|
- lib/ocak/templates/agents/documenter.md.erb
|
|
87
91
|
- lib/ocak/templates/agents/implementer.md.erb
|