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.
@@ -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 'merge handled by MergeManager' if step[:role].to_s == 'merge' && @skip_merge
55
- return 'audit found blocking issues' if step[:role].to_s == 'merge' && @config.audit_mode && state[:audit_blocked]
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
@@ -191,7 +191,7 @@ module Ocak
191
191
  end
192
192
 
193
193
  def detect_test_pass(output)
194
- return true if output.match?(/0 failures,\s*0 errors/)
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 via `gh issue view <number> --json title,body,labels`
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
- ```bash
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.
@@ -10,3 +10,4 @@
10
10
  logs/pipeline/
11
11
  .ocak/logs/
12
12
  .ocak/reports/
13
+ .ocak/trusted
@@ -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
- @worktree_base = File.join(config.project_dir, config.worktree_dir)
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: @config.project_dir)
79
+ Open3.capture3('git', *, chdir: @repo_dir)
79
80
  end
80
81
 
81
82
  def parse_worktree_list(output)
data/lib/ocak.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ocak
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.1'
5
5
 
6
6
  def self.root
7
7
  File.expand_path('..', __dir__)
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.5.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