cleo_quality_review 0.3.0 → 0.5.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.
@@ -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)
@@ -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
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "llm_errors"
6
+
7
+ module CleoQualityReview
8
+ ##
9
+ # Builds the marker and body for the sticky pull request comment from
10
+ # rendered pr_review JSON
11
+ class StickyCommentBuilder
12
+ MARKER_PREFIX = "<!-- cleo-quality-review:"
13
+ MAX_BODY_LENGTH = 3_500
14
+ CLEAN_MESSAGE = "Cleo quality review found no high-confidence issues worth flagging on this change."
15
+
16
+ ##
17
+ # @param [String] rendered_review JSON produced by the pr_review formatter
18
+ def initialize(rendered_review:)
19
+ @rendered_review = rendered_review
20
+ end
21
+
22
+ ##
23
+ # @param [String] commit_sha head commit reviewed to embed in the marker
24
+ # @return [String] full comment body, including the hidden marker
25
+ def comment_body(commit_sha:)
26
+ [marker(commit_sha), truncate(display_body)].join("\n\n")
27
+ end
28
+
29
+ ##
30
+ # @return [Boolean] whether the rendered review has anything worth publishing
31
+ def empty?
32
+ body_text.empty?
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :rendered_review
38
+
39
+ def marker(commit_sha)
40
+ "#{MARKER_PREFIX} commit=#{commit_sha} -->"
41
+ end
42
+
43
+ def display_body
44
+ body_text.empty? ? CLEAN_MESSAGE : body_text
45
+ end
46
+
47
+ def body_text
48
+ parsed_review.fetch("body", "").to_s.strip
49
+ end
50
+
51
+ def parsed_review
52
+ @parsed_review ||= parse_rendered_review
53
+ end
54
+
55
+ # A blank rendered review (e.g. the render step produced no output because
56
+ # there were no reviewable changes) means there is nothing to publish, so
57
+ # treat it as an empty review rather than failing to parse it as JSON.
58
+ def parse_rendered_review
59
+ content = rendered_review.to_s.strip
60
+ return {} if content.empty?
61
+
62
+ parsed = JSON.parse(content)
63
+ raise Error, "pr_review JSON must be an object" unless parsed.is_a?(Hash)
64
+
65
+ parsed
66
+ rescue JSON::ParserError => e
67
+ raise Error, "pr_review output was not valid JSON: #{e.message}"
68
+ end
69
+
70
+ def truncate(value)
71
+ return value if value.length <= MAX_BODY_LENGTH
72
+
73
+ "#{value[0, MAX_BODY_LENGTH - 20]}\n\n[truncated]"
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "github_client"
6
+ require_relative "llm_errors"
7
+ require_relative "sticky_comment_builder"
8
+
9
+ module CleoQualityReview
10
+ ##
11
+ # Publishes quality review findings as a single sticky pull request
12
+ # comment, editing it in place on every run rather than posting a new one
13
+ class StickyCommentPublisher
14
+ COMMENTS_PER_PAGE = 100
15
+ MAX_COMMENT_PAGES = 20
16
+
17
+ ##
18
+ # @param [Run] run completed quality review run
19
+ # @param [String] rendered_review JSON produced by the pr_review formatter
20
+ # @param [Hash{String => String}] env process environment
21
+ # @param [GitHubClient, nil] client GitHub API client (built from env when omitted)
22
+ def initialize(run:, rendered_review:, env: ENV, client: nil)
23
+ @run = run
24
+ @env = env
25
+ @client = client
26
+ @builder = StickyCommentBuilder.new(rendered_review: rendered_review)
27
+ end
28
+
29
+ ##
30
+ # Publish the sticky comment, or skip when there is no PR context/findings
31
+ # @return [String] status message
32
+ def publish
33
+ skip_reason = publication_skip_reason
34
+ return skip_reason if skip_reason
35
+
36
+ existing_comment_id ? update_comment : create_comment
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :env, :run, :builder
42
+
43
+ def publication_skip_reason
44
+ return "No pull_request event found; skipping sticky comment publication." unless pull_request_context?
45
+ return "No sticky comment to publish for review ID #{run.review_id}." if builder.empty? && existing_comment_id.nil?
46
+
47
+ nil
48
+ end
49
+
50
+ def create_comment
51
+ response = client.post(comments_path, { body: comment_body })
52
+ raise Error, "GitHub sticky comment creation failed with status #{response.status_code}: #{response.body}" unless response.success?
53
+
54
+ "Created sticky comment for review ID #{run.review_id}."
55
+ end
56
+
57
+ def update_comment
58
+ response = client.patch(comment_path(existing_comment_id), { body: comment_body })
59
+ raise Error, "GitHub sticky comment update failed with status #{response.status_code}: #{response.body}" unless response.success?
60
+
61
+ "Updated sticky comment for review ID #{run.review_id}."
62
+ end
63
+
64
+ def comment_body
65
+ builder.comment_body(commit_sha: head_sha)
66
+ end
67
+
68
+ def existing_comment_id
69
+ return @existing_comment_id if defined?(@existing_comment_id)
70
+
71
+ @existing_comment_id = find_existing_comment_id
72
+ end
73
+
74
+ def find_existing_comment_id
75
+ marked = comments.find { |comment| bot_authored?(comment) && marked?(comment) }
76
+ marked && marked.fetch("id")
77
+ end
78
+
79
+ def comments
80
+ (1..MAX_COMMENT_PAGES).each_with_object([]) do |page, all|
81
+ page_comments = comments_page(page)
82
+ all.concat(page_comments)
83
+ break all if page_comments.length < COMMENTS_PER_PAGE
84
+ end
85
+ end
86
+
87
+ def comments_page(page)
88
+ response = client.get("#{comments_path}?per_page=#{COMMENTS_PER_PAGE}&page=#{page}")
89
+ raise Error, "GitHub comment lookup failed with status #{response.status_code}: #{response.body}" unless response.success?
90
+
91
+ parsed = JSON.parse(response.body)
92
+ parsed.is_a?(Array) ? parsed : []
93
+ end
94
+
95
+ def bot_authored?(comment)
96
+ comment.dig("user", "type") == "Bot"
97
+ end
98
+
99
+ def marked?(comment)
100
+ comment.fetch("body") { "" }.to_s.include?(StickyCommentBuilder::MARKER_PREFIX)
101
+ end
102
+
103
+ def client
104
+ @client ||= GitHubClient.new(token: token, api_url: api_url)
105
+ end
106
+
107
+ def pull_request_context?
108
+ event.fetch("pull_request", nil).is_a?(Hash)
109
+ end
110
+
111
+ def comments_path
112
+ "/repos/#{repository}/issues/#{pull_request_number}/comments"
113
+ end
114
+
115
+ def comment_path(comment_id)
116
+ "/repos/#{repository}/issues/comments/#{comment_id}"
117
+ end
118
+
119
+ def pull_request_number
120
+ event["number"] || event.fetch("pull_request").fetch("number")
121
+ end
122
+
123
+ def head_sha
124
+ event.fetch("pull_request").fetch("head").fetch("sha")
125
+ end
126
+
127
+ def repository
128
+ env.fetch("GITHUB_REPOSITORY")
129
+ end
130
+
131
+ def api_url
132
+ env.fetch("GITHUB_API_URL", "https://api.github.com")
133
+ end
134
+
135
+ def event
136
+ @event ||= JSON.parse(File.read(env.fetch("GITHUB_EVENT_PATH")))
137
+ end
138
+
139
+ def token
140
+ env.fetch("GITHUB_TOKEN")
141
+ end
142
+ end
143
+ end
@@ -3,5 +3,5 @@
3
3
  module CleoQualityReview
4
4
  ##
5
5
  # Gem version
6
- VERSION = "0.3.0"
6
+ VERSION = "0.5.0"
7
7
  end
data/prompts/agent.md CHANGED
@@ -1,12 +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
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.
10
6
 
11
7
  ## Output Format
12
8
 
@@ -43,10 +39,8 @@ Output valid JSON matching this exact schema:
43
39
  }
44
40
  ```
45
41
 
46
- ## Guidelines
42
+ ## Output rules
47
43
 
48
- 1. Include only findings that exceed thresholds and are actionable
49
- 2. Order findings by priority: high-complexity methods first, then code smells, then performance
50
- 3. Write concise `result` descriptions an agent can act on
51
- 4. Include the raw check outputs in `check_outputs` for reference
52
- 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,22 +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
7
+ You produce useful, meaningful output for the engineer whose PR triggered this flow.
19
8
 
9
+ ## Output Format
20
10
 
21
11
  You MUST NOT return so many items that the feedback is noisy and confusing. Limit yourself to maximum 10 comments.
22
12
 
data/prompts/human.md CHANGED
@@ -1,17 +1,10 @@
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, etc.
4
-
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.
6
-
7
- YOU MUST NOT comment on the code diff itself, unless the comment is in relation 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.
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.
14
6
 
7
+ ## Output Format
15
8
 
16
9
  The output will be printed in a Unix terminal, and so colour-coded feedback is preferable.
17
10
 
data/prompts/pr_review.md CHANGED
@@ -1,61 +1,62 @@
1
- You are the pipeline interface between code quality tools and GitHub pull request review comments.
1
+ You are the pipeline interface between code quality tools and a single, standing summary comment on a GitHub pull request.
2
2
 
3
- You will collate data from code quality tools including Reek, Flog, and Fasterer. The raw output is noisy, so your job is to identify only the most useful comments for the engineer whose PR 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
- You MUST NOT comment on the code diff itself unless the comment is directly supported by a tool finding.
7
+ This comment is edited in place on every push rather than replaced or added to, and the same PR also receives GitHub Actions annotations pointing at the exact file and line of each finding.
8
+ Do not attempt to recreate that per-line detail here.
9
+ Write a short, consolidated narrative of the main themes across findings, then point the reader at the annotations for specifics.
6
10
 
7
- ## Tool Thresholds
11
+ ## Summary Selection
8
12
 
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
-
13
- ## Comment Selection
14
-
15
- 1. Limit yourself to ten comments at most.
16
- 2. Prefer findings that map directly to a changed or commentable right-side line in the git diff.
17
- 3. Omit low-value, duplicated, stale, or ambiguous findings.
18
- 4. If a tool finding points to a file or line that is not visible in the provided diff, omit the inline comment.
19
- 5. Keep comments concise and actionable. Mention the tool and check name.
13
+ 1. Cover at most the five most important themes.
14
+ Group related findings from the same tool or the same underlying cause into one theme rather than listing them separately.
15
+ 2. Mention the tool and check name for each theme, but do not centre the narrative on tool names.
16
+ 3. Do not quote line numbers or file paths; the annotations already carry that detail.
20
17
 
21
18
  ## Output Format
22
19
 
23
- Output ONLY valid JSON. Do not wrap it in markdown fences. Do not include explanatory text before or after the JSON.
20
+ Output ONLY valid JSON.
21
+ Do not wrap it in markdown fences.
22
+ Do not include explanatory text before or after the JSON.
24
23
 
25
24
  The JSON MUST match this schema:
26
25
 
27
26
  ```json
28
27
  {
29
- "body": "<short markdown summary for the PR review body>",
30
- "comments": [
31
- {
32
- "path": "<repository-relative file path>",
33
- "line": <right-side line number from the diff>,
34
- "body": "<markdown review comment>"
35
- }
36
- ]
28
+ "body": "<short markdown narrative summary for the sticky PR comment>"
37
29
  }
38
30
  ```
39
31
 
32
+ ## Comment format
40
33
 
41
- ## Comment format:
34
+ Prioritise readability and actionability.
35
+ Assume the reader is a junior developer, or someone who is not familiar with the language and framework.
36
+ Be helpful, without being overly verbose.
42
37
 
43
- 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.
38
+ Write one short paragraph per theme.
39
+ Separate each theme's paragraph from the next with a blank line - never merge multiple themes into a single paragraph.
40
+ End each theme's paragraph with its own `(Ref: ...)` tag naming the tool and check.
44
41
 
45
42
  Example format:
46
43
  ```
47
- This code appears to have X issue. That may be likely to cause Y problem. Consider an alternative soltion, such as Z.
44
+ This change introduces a fairly complex method that will be expensive to maintain as it grows further.
45
+ Consider breaking it into smaller, named steps.
46
+
47
+ _(Ref: Flog)_
48
+
49
+ There's a repeated pattern here that could be extracted into a shared helper, which would also make the duplication easier to spot next time it happens.
48
50
 
49
- _(Ref: Reek TooManyStatements, DuplicateMethodCall; Fasterer HashKeysEach)_
51
+ _(Ref: Reek DuplicateMethodCall)_
50
52
  ```
51
53
 
52
- ## Empty output:
54
+ ## Empty output
53
55
 
54
- If there are no high-confidence inline comments, return:
56
+ If there are no high-confidence findings worth reporting, return:
55
57
 
56
58
  ```json
57
59
  {
58
- "body": "Cleo quality review did not find any high-confidence issues worth inline PR comments.",
59
- "comments": []
60
+ "body": ""
60
61
  }
61
62
  ```