cleo_quality_review 0.2.0 → 0.4.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.
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "github_client"
6
+ require_relative "llm_errors"
7
+
8
+ module CleoQualityReview
9
+ ##
10
+ # Resolves the git base for an incremental review.
11
+ #
12
+ # On a pull request that cleo-quality-review has already reviewed, this
13
+ # returns the most recent previously-reviewed commit that is still an
14
+ # ancestor of the current head, so only changes made since that review are
15
+ # analysed. It falls back to +nil+ (meaning "review the full diff") outside a
16
+ # pull request context, when no prior review survives in history, or on any
17
+ # lookup error.
18
+ class IncrementalBaseResolver
19
+ REVIEW_MARKER_PREFIX = "<!-- cleo-quality-review:"
20
+ DISABLED_VALUES = %w[0 false no off].freeze
21
+ ENABLED_ENV_KEY = "CLEO_QUALITY_REVIEW_INCREMENTAL"
22
+ REVIEWS_PER_PAGE = 100
23
+ MAX_REVIEW_PAGES = 20
24
+
25
+ ##
26
+ # @param [CommandRunner] command_runner for executing git commands
27
+ # @param [Hash{String => String}] env process environment
28
+ # @param [GitHubClient, nil] client GitHub API client (built from env when omitted)
29
+ def initialize(command_runner:, env: ENV, client: nil)
30
+ @command_runner = command_runner
31
+ @env = env
32
+ @client = client
33
+ end
34
+
35
+ ##
36
+ # Resolve the incremental base commit.
37
+ # @param [String] head git ref for the current head
38
+ # @return [String, nil] commit SHA to diff against, or nil to review the full diff
39
+ def resolve(head: "HEAD")
40
+ return nil unless incremental_lookup_available?
41
+
42
+ newest_reviewed_ancestor(head)
43
+ rescue StandardError => error
44
+ warn("cleo-quality-review: incremental base lookup failed (#{error.message}); reviewing the full diff")
45
+ nil
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :command_runner, :env
51
+
52
+ ##
53
+ # @return [Boolean] whether an incremental lookup can run in this context
54
+ def incremental_lookup_available?
55
+ enabled? && !pull_request_number.nil? && !token.nil? && !repository.nil?
56
+ end
57
+
58
+ def newest_reviewed_ancestor(head)
59
+ reviewed_commit_ids.find { |sha| ancestor?(sha, head) }
60
+ end
61
+
62
+ def reviewed_commit_ids
63
+ reviews
64
+ .select { |review| quality_review?(review) }
65
+ .sort_by { |review| review["submitted_at"].to_s }
66
+ .reverse
67
+ .filter_map { |review| review["commit_id"] }
68
+ .reject { |sha| sha.to_s.strip.empty? }
69
+ .uniq
70
+ end
71
+
72
+ ##
73
+ # Fetch every submitted review, following pagination so the newest reviews
74
+ # are not missed on pull requests with more than one page of reviews.
75
+ # @return [Array<Hash>]
76
+ def reviews
77
+ (1..MAX_REVIEW_PAGES).each_with_object([]) do |page, all|
78
+ page_reviews = reviews_page(page)
79
+ all.concat(page_reviews)
80
+ break all if page_reviews.length < REVIEWS_PER_PAGE
81
+ end
82
+ end
83
+
84
+ def reviews_page(page)
85
+ response = client.get("/repos/#{repository}/pulls/#{pull_request_number}/reviews?per_page=#{REVIEWS_PER_PAGE}&page=#{page}")
86
+ raise Error, "GitHub review lookup returned status #{response.status_code}" unless response.success?
87
+
88
+ parsed = JSON.parse(response.body)
89
+ parsed.is_a?(Array) ? parsed : []
90
+ end
91
+
92
+ ##
93
+ # Only trust bot-authored reviews that carry our marker. A human contributor
94
+ # could otherwise forge the marker in their own review and steer the base
95
+ # past changes the tool never analysed.
96
+ # @param [Hash] review
97
+ # @return [Boolean]
98
+ def quality_review?(review)
99
+ bot_authored?(review) && marked?(review)
100
+ end
101
+
102
+ def bot_authored?(review)
103
+ review.dig("user", "type") == "Bot"
104
+ end
105
+
106
+ def marked?(review)
107
+ review.fetch("body") { "" }.to_s.include?(REVIEW_MARKER_PREFIX)
108
+ end
109
+
110
+ def ancestor?(sha, head)
111
+ command_runner.run("git", "merge-base", "--is-ancestor", sha, head).success?
112
+ end
113
+
114
+ def enabled?
115
+ !DISABLED_VALUES.include?(env.fetch(ENABLED_ENV_KEY) { "" }.to_s.strip.downcase)
116
+ end
117
+
118
+ def pull_request_number
119
+ return @pull_request_number if defined?(@pull_request_number)
120
+
121
+ @pull_request_number = event && (event["number"] || event.dig("pull_request", "number"))
122
+ end
123
+
124
+ def event
125
+ return @event if defined?(@event)
126
+
127
+ @event = load_event
128
+ end
129
+
130
+ def load_event
131
+ path = env["GITHUB_EVENT_PATH"]
132
+ return nil if path.to_s.empty? || !File.file?(path)
133
+
134
+ JSON.parse(File.read(path))
135
+ rescue JSON::ParserError
136
+ nil
137
+ end
138
+
139
+ def token
140
+ value = env["GITHUB_TOKEN"].to_s
141
+ value unless value.empty?
142
+ end
143
+
144
+ def repository
145
+ value = env["GITHUB_REPOSITORY"].to_s
146
+ value unless value.empty?
147
+ end
148
+
149
+ def api_url
150
+ env.fetch("GITHUB_API_URL") { GitHubClient::DEFAULT_API_URL }
151
+ end
152
+
153
+ def client
154
+ @client ||= GitHubClient.new(token: token, api_url: api_url)
155
+ end
156
+ end
157
+ end
@@ -19,10 +19,12 @@ module CleoQualityReview
19
19
 
20
20
  ##
21
21
  # Generate a review from the given prompt
22
- # @param [String] prompt
22
+ # @param [String] prompt the format-specific prompt sent as input
23
+ # @param [String, nil] instructions shared configuration prompt applied to
24
+ # every run
23
25
  # @return [String] the generated review
24
- def generate_review(prompt)
25
- generate_with_logging(prompt)
26
+ def generate_review(prompt, instructions: nil)
27
+ generate_with_logging(prompt, instructions)
26
28
  rescue StandardError => e
27
29
  log_error(prompt, e)
28
30
  raise
@@ -32,8 +34,10 @@ module CleoQualityReview
32
34
 
33
35
  attr_reader :config, :logger
34
36
 
35
- def generate_with_logging(prompt)
36
- provider_client.generate_review(prompt).tap { |response| log_success(prompt, response) }
37
+ def generate_with_logging(prompt, instructions)
38
+ provider_client.generate_review(prompt, instructions: instructions).tap do |response|
39
+ log_success(prompt, response)
40
+ end
37
41
  end
38
42
 
39
43
  def log_success(prompt, response)
@@ -89,11 +89,13 @@ module CleoQualityReview
89
89
 
90
90
  ##
91
91
  # Generate a review using the OpenAI Responses API.
92
- # @param [String] prompt the prompt to send
92
+ # @param [String] prompt the format-specific prompt to send as input
93
+ # @param [String, nil] instructions shared configuration prompt sent as
94
+ # the system-level instructions applied to every run
93
95
  # @return [String] generated review text
94
96
  # @raise [ApiError] if the API request fails
95
- def generate_review(prompt)
96
- response = execute_request(prompt)
97
+ def generate_review(prompt, instructions: nil)
98
+ response = execute_request(request_body(prompt, instructions))
97
99
  parse_response(response)
98
100
  end
99
101
 
@@ -101,9 +103,9 @@ module CleoQualityReview
101
103
 
102
104
  attr_reader :config, :http_transport
103
105
 
104
- def execute_request(prompt)
106
+ def execute_request(body)
105
107
  timeout_seconds = config.timeout_seconds
106
- http_transport.post_json(build_request(prompt, timeout_seconds))
108
+ http_transport.post_json(build_request(body, timeout_seconds))
107
109
  rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout => e
108
110
  raise ApiError, timeout_error_message(timeout_seconds, e)
109
111
  end
@@ -116,15 +118,21 @@ module CleoQualityReview
116
118
  raise ApiError, "OpenAI Responses API returned invalid JSON: #{e.message}"
117
119
  end
118
120
 
119
- def build_request(prompt, timeout_seconds)
121
+ def build_request(body, timeout_seconds)
120
122
  HttpRequest.new(
121
123
  uri: RESPONSES_API_URL,
122
124
  headers: headers,
123
- body: { model: config.model, input: prompt },
125
+ body: body,
124
126
  timeout_seconds: timeout_seconds,
125
127
  )
126
128
  end
127
129
 
130
+ def request_body(prompt, instructions)
131
+ body = { model: config.model, input: prompt }
132
+ body[:instructions] = instructions unless instructions.to_s.strip.empty?
133
+ body
134
+ end
135
+
128
136
  def timeout_error_message(timeout_seconds, error)
129
137
  "OpenAI Responses API request timed out after #{timeout_seconds} seconds: #{error.class}: #{error.message}"
130
138
  end
@@ -54,21 +54,25 @@ module CleoQualityReview
54
54
  ##
55
55
  # Stub LLM client, mirrors OpenAi::Client interface.
56
56
  class Client
57
- attr_reader :received_prompts
57
+ attr_reader :received_prompts, :received_instructions
58
58
 
59
59
  ##
60
60
  # @param [Config] config stub configuration
61
61
  def initialize(config:)
62
62
  @config = config
63
63
  @received_prompts = []
64
+ @received_instructions = []
64
65
  end
65
66
 
66
67
  ##
67
68
  # Generate a review by returning the configured response.
68
- # @param [String] prompt the prompt sent
69
+ # @param [String] prompt the format-specific prompt sent as input
70
+ # @param [String, nil] instructions shared configuration prompt applied
71
+ # to every run
69
72
  # @return [String] the configured response
70
- def generate_review(prompt)
73
+ def generate_review(prompt, instructions: nil)
71
74
  received_prompts << prompt
75
+ received_instructions << instructions
72
76
  response = config.response
73
77
 
74
78
  case response
@@ -25,7 +25,9 @@ module CleoQualityReview
25
25
  # @return [Array<String>] checks to exclude
26
26
  # @!attribute [r] changed
27
27
  # @return [Boolean] whether to filter to changed files only
28
- ParseResult = Struct.new(:format, :checks, :files, :exclude, :changed, :base, :log, :review_id, :review_file, keyword_init: true) do
28
+ # @!attribute [r] jobs
29
+ # @return [Integer, nil] max checks to run in parallel, or nil to auto-size
30
+ ParseResult = Struct.new(:format, :checks, :files, :exclude, :changed, :base, :log, :review_id, :review_file, :jobs, keyword_init: true) do
29
31
  ##
30
32
  # @return [String] validated review_id
31
33
  # @raise [OptionParser::MissingArgument] if review_id is blank
@@ -64,6 +66,7 @@ module CleoQualityReview
64
66
  @log = false
65
67
  @review_id = nil
66
68
  @review_file = nil
69
+ @jobs = nil
67
70
  end
68
71
 
69
72
  ##
@@ -85,17 +88,19 @@ module CleoQualityReview
85
88
  log: log,
86
89
  review_id: review_id,
87
90
  review_file: review_file,
91
+ jobs: jobs,
88
92
  )
89
93
  end
90
94
 
91
95
  private
92
96
 
93
- attr_reader :argv, :format, :checks, :files, :exclude, :changed, :base, :log, :review_id, :review_file
97
+ attr_reader :argv, :format, :checks, :files, :exclude, :changed, :base, :log, :review_id, :review_file, :jobs
94
98
 
95
99
  def parser
96
100
  OptionParser.new do |opts|
97
101
  opts.banner = "Usage: check_quality [options] [files...]"
98
102
  register_options(opts)
103
+ register_help_option(opts)
99
104
  end
100
105
  end
101
106
 
@@ -104,7 +109,13 @@ module CleoQualityReview
104
109
  register_check_options(opts)
105
110
  register_target_options(opts)
106
111
  register_output_options(opts)
107
- register_help_option(opts)
112
+ register_jobs_option(opts)
113
+ end
114
+
115
+ def register_jobs_option(opts)
116
+ opts.on("-j", "--jobs N", Integer, "Max checks to run in parallel (default: CPU cores)") do |value|
117
+ @jobs = value
118
+ end
108
119
  end
109
120
 
110
121
  def register_format_option(opts)
@@ -120,7 +131,7 @@ module CleoQualityReview
120
131
  end
121
132
 
122
133
  def register_checks_option(opts)
123
- opts.on("-c", "--checks CHECKS", Array, "Checks to run: all, reek, flog, fasterer, debride") { |values| checks.concat(values) }
134
+ opts.on("-c", "--checks CHECKS", Array, "Checks to run: all, reek, flog, fasterer") { |values| checks.concat(values) }
124
135
  end
125
136
 
126
137
  def register_only_option(opts)
@@ -128,7 +139,7 @@ module CleoQualityReview
128
139
  end
129
140
 
130
141
  def register_exclude_option(opts)
131
- opts.on("-x", "--exclude CHECKS", Array, "Checks to exclude: reek, flog, fasterer, debride") { |values| exclude.concat(values) }
142
+ opts.on("-x", "--exclude CHECKS", Array, "Checks to exclude: reek, flog, fasterer") { |values| exclude.concat(values) }
132
143
  end
133
144
 
134
145
  def register_target_options(opts)
@@ -38,6 +38,14 @@ module CleoQualityReview
38
38
  :log,
39
39
  keyword_init: true,
40
40
  ) do
41
+ ##
42
+ # Whether the run has any files to review. Runs with no target files
43
+ # (e.g. a branch that only changes non-Ruby files) have nothing to analyse.
44
+ # @return [Boolean]
45
+ def reviewable?
46
+ !Array(target_files).empty?
47
+ end
48
+
41
49
  ##
42
50
  # Convert the run to a hash representation
43
51
  # @return [Hash{Symbol => Object}]
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "digest"
4
+ require "forwardable"
4
5
  require "json"
5
6
 
6
7
  require_relative "changes_diff"
7
8
  require_relative "checks"
8
9
  require_relative "command_runner"
10
+ require_relative "concurrent_executor"
9
11
  require_relative "git_diff_base"
10
12
  require_relative "run"
11
13
  require_relative "run_artifacts"
@@ -15,6 +17,8 @@ module CleoQualityReview
15
17
  ##
16
18
  # Orchestrates a complete quality review run
17
19
  class Runner
20
+ extend Forwardable
21
+
18
22
  ##
19
23
  # Grouped values resolved at the start of an analysis run
20
24
  AnalysisContext = Struct.new(:timestamp, :base_ref, :target, :changes, :review_id, :check_classes, keyword_init: true) do
@@ -32,16 +36,28 @@ module CleoQualityReview
32
36
  end
33
37
  end
34
38
 
39
+ ##
40
+ # Runtime collaborators for a quality review run
41
+ Dependencies = Struct.new(:command_runner, :clock, :check_registry, :base_resolver, :executor, keyword_init: true) do
42
+ def self.for(options, overrides)
43
+ new(
44
+ **{
45
+ command_runner: CommandRunner.new,
46
+ clock: Time,
47
+ check_registry: Checks,
48
+ base_resolver: nil,
49
+ executor: ConcurrentExecutor.new(max_workers: options.jobs),
50
+ }.merge(overrides),
51
+ )
52
+ end
53
+ end
54
+
35
55
  ##
36
56
  # @param [Options::ParseResult] options parsed command-line options
37
- # @param [CommandRunner] command_runner for executing shell commands
38
- # @param [#now] clock time source for timestamps
39
- # @param [CheckRegistry] check_registry registry for resolving check names
40
- def initialize(options:, command_runner: CommandRunner.new, clock: Time, check_registry: Checks)
57
+ # @param [Hash] dependencies optional runtime collaborators for tests or alternate runners
58
+ def initialize(options:, **dependencies)
41
59
  @options = options
42
- @command_runner = command_runner
43
- @clock = clock
44
- @check_registry = check_registry
60
+ @dependencies = Dependencies.for(options, dependencies)
45
61
  end
46
62
 
47
63
  ##
@@ -57,7 +73,9 @@ module CleoQualityReview
57
73
 
58
74
  private
59
75
 
60
- attr_reader :options, :command_runner, :clock, :check_registry
76
+ attr_reader :options, :dependencies
77
+ def_delegators :dependencies, :command_runner, :clock, :check_registry, :base_resolver, :executor
78
+ private :command_runner, :clock, :check_registry, :base_resolver, :executor
61
79
 
62
80
  def epoch_milliseconds
63
81
  (clock.now.to_r * 1_000).to_i
@@ -127,7 +145,9 @@ module CleoQualityReview
127
145
  end
128
146
 
129
147
  def run_checks(check_classes, ruby_files, timestamp)
130
- check_classes.map do |check_class|
148
+ return [] if ruby_files.empty?
149
+
150
+ executor.map(check_classes) do |check_class|
131
151
  check_class.new(command_runner: command_runner, timestamp: timestamp).run(ruby_files)
132
152
  end
133
153
  end
@@ -170,6 +190,10 @@ module CleoQualityReview
170
190
  end
171
191
 
172
192
  def base_ref
193
+ @base_ref ||= base_resolver&.resolve || default_base_ref
194
+ end
195
+
196
+ def default_base_ref
173
197
  options.base || GitDiffBase::DEFAULT_BASE_REF
174
198
  end
175
199
  end
@@ -3,5 +3,5 @@
3
3
  module CleoQualityReview
4
4
  ##
5
5
  # Gem version
6
- VERSION = "0.2.0"
6
+ VERSION = "0.4.0"
7
7
  end
@@ -14,7 +14,6 @@ module CleoQualityReview
14
14
  Checks.register("Reek", Checks::Reek, tool_type: :smell_detection)
15
15
  Checks.register("Flog", Checks::Flog, tool_type: :complexity)
16
16
  Checks.register("Fasterer", Checks::Fasterer, tool_type: :performance)
17
- Checks.register("Debride", Checks::Debride, tool_type: :dead_code)
18
17
 
19
18
  ##
20
19
  # Register all supported LLM APIs for formatting output here
data/prompts/agent.md CHANGED
@@ -1,13 +1,8 @@
1
1
  You are reviewing Ruby code quality findings for consumption by AI coding assistants.
2
2
 
3
- Analyze the raw tool outputs and git diff provided. Prioritize actionable issues affecting maintainability, readability, performance, and complexity. Filter out low-signal findings.
4
-
5
- ## Tool Thresholds
6
-
7
- - **Flog**: Ignore scores below 40.0
8
- - **Reek**: Focus on FeatureEnvy, TooManyStatements, DuplicateMethodCall, NestedIterators, LongParameterList
9
- - **Fasterer**: Include all performance suggestions
10
- - **Debride**: Treat as lower-confidence static dead-code detection. Include only findings that are clearly actionable and avoid recommending deletion without checking dynamic call paths.
3
+ Apply the shared review rules from the configuration prompt provided alongside this one.
4
+ That prompt defines the inputs, tool thresholds, prioritisation, and noise-reduction rules.
5
+ This prompt defines only the output format.
11
6
 
12
7
  ## Output Format
13
8
 
@@ -21,7 +16,7 @@ Output valid JSON matching this exact schema:
21
16
  "target_files": [<file paths from metadata>],
22
17
  "findings": [
23
18
  {
24
- "tool_name": "<reek|flog|fasterer|debride>",
19
+ "tool_name": "<reek|flog|fasterer>",
25
20
  "tool_type": "<smell_detection|complexity|performance|dead_code>",
26
21
  "check": "<specific check type>",
27
22
  "filepath": "<relative file path>",
@@ -33,7 +28,7 @@ Output valid JSON matching this exact schema:
33
28
  "check_outputs": [
34
29
  {
35
30
  "check_name": "<check name>",
36
- "tool_name": "<reek|flog|fasterer|debride>",
31
+ "tool_name": "<reek|flog|fasterer>",
37
32
  "tool_type": "<smell_detection|complexity|performance|dead_code>",
38
33
  "extension": "<json|txt>",
39
34
  "path": "<raw output artifact path>",
@@ -44,10 +39,8 @@ Output valid JSON matching this exact schema:
44
39
  }
45
40
  ```
46
41
 
47
- ## Guidelines
42
+ ## Output rules
48
43
 
49
- 1. Include only findings that exceed thresholds and are actionable
50
- 2. Order findings by priority: high-complexity methods first, then code smells, then performance
51
- 3. Write concise `result` descriptions an agent can act on
52
- 4. Include the raw check outputs in `check_outputs` for reference
53
- 5. Output ONLY valid JSON - no markdown fences, no explanatory text
44
+ 1. Write concise `result` descriptions an agent can act on.
45
+ 2. Include the raw check outputs in `check_outputs` for reference.
46
+ 3. Output ONLY valid JSON - no markdown fences, no explanatory text.
@@ -0,0 +1,47 @@
1
+ You are reviewing Ruby code quality findings produced by static analysis tools.
2
+
3
+ These are the standard rules that apply to every review, regardless of the output format.
4
+ The output format is defined separately in the format-specific prompt that accompanies these rules.
5
+
6
+ ## Inputs
7
+
8
+ You are given the raw output from a series of code quality tools (including, but not limited to, Reek, Flog, Fasterer, Flay, and Brakeman), together with the git diff for the change under review.
9
+ The combined tool output is noisy.
10
+ Your job is to decide what genuinely matters and to discard the rest.
11
+ The diff is provided so you can map tool findings to the lines that changed.
12
+
13
+ ## Excluded files
14
+
15
+ Do not review test files: ignore every tool finding that points to one, and never post a comment on a test file.
16
+ Test files are those inside a `test/` or `spec/` directory, for example `test/models/user_test.rb`.
17
+ A file that merely has `test` in its name but lives in application code, such as an A/B-test model under `app/`, is not a test file.
18
+ Tests in this codebase are intentionally verbose and self-contained, so the smells these tools report on them are expected rather than defects.
19
+
20
+ ## Tool thresholds and severity
21
+
22
+ - **Flog**: Ignore scores below 40.0. Treat high-complexity methods as the most important findings because they are the most expensive to maintain.
23
+ - **Reek**: Prefer actionable smells such as FeatureEnvy, DuplicateMethodCall, NestedIterators, and LongParameterList.
24
+ - **Fasterer**: Low severity. Include a performance suggestion only when it clearly applies to code changed by this review and the fix is straightforward.
25
+
26
+ ## Rule-specific guidance
27
+
28
+ These notes refine how individual rules should be treated.
29
+ Where a note here conflicts with the general guidance above, the note takes precedence for that rule.
30
+
31
+ ### Reek: TooManyStatements
32
+
33
+ - Deprioritise this smell in application code.
34
+ Only surface it when the method is a particularly egregious example, such as a long method that clearly juggles several unrelated responsibilities, and omit it otherwise.
35
+
36
+ ## Prioritisation
37
+
38
+ 1. Prioritise issues that affect maintainability, correctness, readability, performance, and long-term ownership.
39
+ 2. Order findings by impact: high-complexity methods first, then code smells, then performance suggestions.
40
+ 3. Filter out low-signal findings.
41
+
42
+ ## Noise reduction
43
+
44
+ - Do not comment on the code diff itself unless the comment is directly supported by a tool finding.
45
+ - Do not repeat tool output mechanically. When several findings are of the same kind, highlight a couple of representative examples and then make one general recommendation.
46
+ - If a finding is low value, stale, ambiguous, or a likely false positive, omit it or note it briefly.
47
+ - Keep every finding concise and actionable, specific enough for an engineer or coding agent to act on.
data/prompts/github.md CHANGED
@@ -1,26 +1,12 @@
1
1
  You are the pipeline interface between a series of code reviews for a git diff, and the GitHub Actions automation pipeline.
2
2
 
3
- You will collate data about code from multiple code sources (including, but not limited to Flog, Flay, Reek, Fasterer, Brakeman, etc.), and produce useful, meaningful output for the engineer whose PR has triggered this flow.
3
+ Apply the shared review rules from the configuration prompt provided alongside this one.
4
+ That prompt defines the inputs, tool thresholds, prioritisation, and noise-reduction rules.
5
+ This prompt defines only the output format.
4
6
 
5
- The output from all of these reports together is very noisy, and so your role is to determine what is the most important things to report back on the PR, and what items can be disregarded.
6
-
7
- For weighting, consider the following values as guides:
8
-
9
- Flog:
10
- Threshold: 40.0
11
- ThresholdType: GreaterThanOrEqual
12
- Severity: Medium to High
13
-
14
- Reek:
15
- Severity: Low to Medium
16
-
17
- Fasterer:
18
- Severity: Low
19
-
20
- Debride:
21
- Severity: Low
22
- Notes: Lower-confidence static dead-code signal. Only report when the finding is specific, actionable, and unlikely to be a dynamic Rails call.
7
+ You produce useful, meaningful output for the engineer whose PR triggered this flow.
23
8
 
9
+ ## Output Format
24
10
 
25
11
  You MUST NOT return so many items that the feedback is noisy and confusing. Limit yourself to maximum 10 comments.
26
12
 
data/prompts/human.md CHANGED
@@ -1,20 +1,12 @@
1
1
  You are reviewing a local code change for code quality.
2
2
 
3
- The files provided include git diffs for local code changes, as well as generated output files from various code quality assessment tools including (but not limited to) Reek, Flog, Fasterer, Debride, etc.
3
+ Apply the shared review rules from the configuration prompt provided alongside this one.
4
+ That prompt defines the inputs, tool thresholds, prioritisation, and noise-reduction rules.
5
+ This prompt defines only the output format.
4
6
 
5
- Your task is to parse the static output files generated by these tools, and provide feedback to the human user. The diff provided is to allow you to map tool output to changes in the code.
7
+ ## Output Format
6
8
 
7
- YOU MUST NOT comment on the code diff itself, unless the comment is in relatinon to an issue reported by a tool.
8
-
9
- Prioritize issues that are likely to matter to maintainability, correctness, readability, or long-term ownership.
10
-
11
- Avoid repeating tool output mechanically. If multiple issues of the same sort are reported, it's fine to highlight a couple of examples and then make a general comment for improvement.
12
-
13
- If a tool finding is low value or likely a false positive, say so briefly or omit it.
14
-
15
- Debride findings are lower-confidence static dead-code candidates. Do not recommend deleting code unless the finding is clearly supported by the changed code and dynamic call paths have been considered.
16
-
17
- The output will be printed in a unix terminal, and so colour-coded feedback is preferrable.
9
+ The output will be printed in a Unix terminal, and so colour-coded feedback is preferable.
18
10
 
19
11
  1. Highest-impact issues first, with file and line references as clickable links when available.
20
12
  2. Suggested changes that are specific enough for an engineer or coding agent to implement.
data/prompts/pr_review.md CHANGED
@@ -1,23 +1,15 @@
1
1
  You are the pipeline interface between code quality tools and GitHub pull request review comments.
2
2
 
3
- You will collate data from code quality tools including Reek, Flog, Fasterer, and Debride. The raw output is noisy, so your job is to identify only the most useful comments for the engineer whose PR triggered this flow.
4
-
5
- You MUST NOT comment on the code diff itself unless the comment is directly supported by a tool finding.
6
-
7
- ## Tool Thresholds
8
-
9
- - **Flog**: Ignore scores below 40.0. Prioritize high-complexity methods because they are the most expensive to maintain.
10
- - **Reek**: Prefer actionable smells such as FeatureEnvy, TooManyStatements, DuplicateMethodCall, NestedIterators, and LongParameterList.
11
- - **Fasterer**: Low severity. Include only when the finding is clearly on code changed by this PR and the fix is straightforward.
12
- - **Debride**: Lower-confidence static dead-code signal. Include only when the candidate method is clearly made obsolete by this PR, and do not suggest deletion without noting possible dynamic Rails calls.
3
+ Apply the shared review rules from the configuration prompt provided alongside this one.
4
+ That prompt defines the inputs, tool thresholds, prioritisation, and noise-reduction rules.
5
+ This prompt defines only the output format.
13
6
 
14
7
  ## Comment Selection
15
8
 
16
9
  1. Limit yourself to ten comments at most.
17
10
  2. Prefer findings that map directly to a changed or commentable right-side line in the git diff.
18
- 3. Omit low-value, duplicated, stale, or ambiguous findings.
19
- 4. If a tool finding points to a file or line that is not visible in the provided diff, omit the inline comment.
20
- 5. Keep comments concise and actionable. Mention the tool and check name.
11
+ 3. If a tool finding points to a file or line that is not visible in the provided diff, omit the inline comment.
12
+ 4. Mention the tool and check name in each comment.
21
13
 
22
14
  ## Output Format
23
15
 
@@ -41,7 +33,7 @@ The JSON MUST match this schema:
41
33
 
42
34
  ## Comment format:
43
35
 
44
- The comments should prioritise readability and actionabilty. Assume the reader is a junior developer, or someone who is not familiar with the language and framework. Be helpful, without being overly verbose.
36
+ The comments should prioritise readability and actionabilty. Assume the reader is a junior developer, or someone who is not familiar with the language and framework. Be helpful, without being overly verbose.
45
37
 
46
38
  Example format:
47
39
  ```