ace-review 0.49.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 +7 -0
- data/.ace-defaults/nav/protocols/guide-sources/ace-review.yml +10 -0
- data/.ace-defaults/nav/protocols/prompt-sources/ace-review.yml +36 -0
- data/.ace-defaults/nav/protocols/tmpl-sources/ace-review.yml +10 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-review.yml +19 -0
- data/.ace-defaults/review/config.yml +79 -0
- data/.ace-defaults/review/presets/code-fit.yml +64 -0
- data/.ace-defaults/review/presets/code-shine.yml +44 -0
- data/.ace-defaults/review/presets/code-valid.yml +39 -0
- data/.ace-defaults/review/presets/docs.yml +42 -0
- data/.ace-defaults/review/presets/spec.yml +37 -0
- data/CHANGELOG.md +1780 -0
- data/LICENSE +21 -0
- data/README.md +42 -0
- data/Rakefile +14 -0
- data/exe/ace-review +27 -0
- data/exe/ace-review-feedback +17 -0
- data/handbook/guides/code-review-process.g.md +234 -0
- data/handbook/prompts/base/sections.md +23 -0
- data/handbook/prompts/base/system.md +60 -0
- data/handbook/prompts/focus/architecture/atom.md +30 -0
- data/handbook/prompts/focus/architecture/reflection.md +60 -0
- data/handbook/prompts/focus/frameworks/rails.md +40 -0
- data/handbook/prompts/focus/frameworks/vue-firebase.md +45 -0
- data/handbook/prompts/focus/languages/ruby.md +50 -0
- data/handbook/prompts/focus/phase/correctness.md +51 -0
- data/handbook/prompts/focus/phase/polish.md +43 -0
- data/handbook/prompts/focus/phase/quality.md +42 -0
- data/handbook/prompts/focus/quality/performance.md +48 -0
- data/handbook/prompts/focus/quality/security.md +47 -0
- data/handbook/prompts/focus/scope/docs.md +38 -0
- data/handbook/prompts/focus/scope/spec.md +58 -0
- data/handbook/prompts/focus/scope/tests.md +36 -0
- data/handbook/prompts/format/compact.md +12 -0
- data/handbook/prompts/format/detailed.md +39 -0
- data/handbook/prompts/format/standard.md +16 -0
- data/handbook/prompts/guidelines/icons.md +19 -0
- data/handbook/prompts/guidelines/tone.md +21 -0
- data/handbook/prompts/synthesis-review-reports.system.md +318 -0
- data/handbook/prompts/synthesize-feedback.system.md +147 -0
- data/handbook/skills/as-review-apply-feedback/SKILL.md +39 -0
- data/handbook/skills/as-review-package/SKILL.md +36 -0
- data/handbook/skills/as-review-pr/SKILL.md +38 -0
- data/handbook/skills/as-review-run/SKILL.md +30 -0
- data/handbook/skills/as-review-verify-feedback/SKILL.md +31 -0
- data/handbook/templates/review-tasks/task-review-summary.template.md +148 -0
- data/handbook/workflow-instructions/review/apply-feedback.wf.md +212 -0
- data/handbook/workflow-instructions/review/package.wf.md +16 -0
- data/handbook/workflow-instructions/review/pr.wf.md +284 -0
- data/handbook/workflow-instructions/review/run.wf.md +262 -0
- data/handbook/workflow-instructions/review/verify-feedback.wf.md +286 -0
- data/lib/ace/review/atoms/context_limit_resolver.rb +162 -0
- data/lib/ace/review/atoms/diff_boundary_finder.rb +133 -0
- data/lib/ace/review/atoms/feedback_id_generator.rb +66 -0
- data/lib/ace/review/atoms/feedback_slug_generator.rb +61 -0
- data/lib/ace/review/atoms/feedback_state_validator.rb +98 -0
- data/lib/ace/review/atoms/pr_comment_formatter.rb +325 -0
- data/lib/ace/review/atoms/preset_validator.rb +103 -0
- data/lib/ace/review/atoms/priority_filter.rb +115 -0
- data/lib/ace/review/atoms/retry_with_backoff.rb +75 -0
- data/lib/ace/review/atoms/slug_generator.rb +50 -0
- data/lib/ace/review/atoms/token_estimator.rb +86 -0
- data/lib/ace/review/cli/commands/feedback/create.rb +173 -0
- data/lib/ace/review/cli/commands/feedback/list.rb +280 -0
- data/lib/ace/review/cli/commands/feedback/resolve.rb +109 -0
- data/lib/ace/review/cli/commands/feedback/session_discovery.rb +70 -0
- data/lib/ace/review/cli/commands/feedback/show.rb +177 -0
- data/lib/ace/review/cli/commands/feedback/skip.rb +125 -0
- data/lib/ace/review/cli/commands/feedback/verify.rb +149 -0
- data/lib/ace/review/cli/commands/feedback.rb +79 -0
- data/lib/ace/review/cli/commands/review.rb +378 -0
- data/lib/ace/review/cli/feedback_cli.rb +71 -0
- data/lib/ace/review/cli.rb +103 -0
- data/lib/ace/review/errors.rb +146 -0
- data/lib/ace/review/models/feedback_item.rb +216 -0
- data/lib/ace/review/models/review_options.rb +208 -0
- data/lib/ace/review/models/reviewer.rb +181 -0
- data/lib/ace/review/molecules/context_composer.rb +123 -0
- data/lib/ace/review/molecules/context_extractor.rb +159 -0
- data/lib/ace/review/molecules/feedback_directory_manager.rb +183 -0
- data/lib/ace/review/molecules/feedback_file_reader.rb +178 -0
- data/lib/ace/review/molecules/feedback_file_writer.rb +210 -0
- data/lib/ace/review/molecules/feedback_synthesizer.rb +588 -0
- data/lib/ace/review/molecules/gh_cli_executor.rb +124 -0
- data/lib/ace/review/molecules/gh_comment_poster.rb +205 -0
- data/lib/ace/review/molecules/gh_comment_resolver.rb +199 -0
- data/lib/ace/review/molecules/gh_pr_comment_fetcher.rb +408 -0
- data/lib/ace/review/molecules/gh_pr_fetcher.rb +240 -0
- data/lib/ace/review/molecules/llm_executor.rb +142 -0
- data/lib/ace/review/molecules/multi_model_executor.rb +278 -0
- data/lib/ace/review/molecules/nav_prompt_resolver.rb +145 -0
- data/lib/ace/review/molecules/pr_task_spec_resolver.rb +58 -0
- data/lib/ace/review/molecules/preset_manager.rb +494 -0
- data/lib/ace/review/molecules/prompt_composer.rb +76 -0
- data/lib/ace/review/molecules/prompt_resolver.rb +168 -0
- data/lib/ace/review/molecules/strategies/adaptive_strategy.rb +193 -0
- data/lib/ace/review/molecules/strategies/chunked_strategy.rb +459 -0
- data/lib/ace/review/molecules/strategies/full_strategy.rb +114 -0
- data/lib/ace/review/molecules/subject_extractor.rb +315 -0
- data/lib/ace/review/molecules/subject_filter.rb +199 -0
- data/lib/ace/review/molecules/subject_strategy.rb +96 -0
- data/lib/ace/review/molecules/task_report_saver.rb +161 -0
- data/lib/ace/review/molecules/task_resolver.rb +48 -0
- data/lib/ace/review/organisms/feedback_manager.rb +386 -0
- data/lib/ace/review/organisms/review_manager.rb +1059 -0
- data/lib/ace/review/version.rb +7 -0
- data/lib/ace/review.rb +135 -0
- metadata +351 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Review
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
# ace-support-cli Command class for the review command
|
|
10
|
+
#
|
|
11
|
+
# Executes code review using presets or custom configuration.
|
|
12
|
+
class Review < Ace::Support::Cli::Command
|
|
13
|
+
include Ace::Support::Cli::Base
|
|
14
|
+
|
|
15
|
+
desc <<~DESC.strip
|
|
16
|
+
Execute code review using presets or custom configuration
|
|
17
|
+
|
|
18
|
+
Presets provide pre-configured review types with focused prompts:
|
|
19
|
+
code → General code review
|
|
20
|
+
code-pr → PR-focused code review
|
|
21
|
+
security → Security-focused review
|
|
22
|
+
performance → Performance-focused review
|
|
23
|
+
docs → Documentation review
|
|
24
|
+
|
|
25
|
+
Configuration:
|
|
26
|
+
Global config: ~/.ace/review/config.yml
|
|
27
|
+
Project config: .ace/review/config.yml
|
|
28
|
+
Example: ace-review/.ace-defaults/review/config.yml
|
|
29
|
+
|
|
30
|
+
Presets configured via review.presets
|
|
31
|
+
DESC
|
|
32
|
+
|
|
33
|
+
example [
|
|
34
|
+
"--preset code-pr # PR code review",
|
|
35
|
+
"--preset security --auto-execute # Run and apply fixes",
|
|
36
|
+
"--pr 123 # Review by PR number",
|
|
37
|
+
"--preset code --subject diff:HEAD~3 --subject files:docs/**/*.md",
|
|
38
|
+
"--preset security --dry-run # Preview without executing"
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# Review configuration options
|
|
42
|
+
option :preset, type: :string, desc: "Review preset from configuration"
|
|
43
|
+
option :output_dir, type: :string, desc: "Custom output directory for review"
|
|
44
|
+
option :output, type: :string, desc: "Specific output file path"
|
|
45
|
+
option :context, type: :string, desc: "Context configuration (preset name or YAML)"
|
|
46
|
+
option :subject, type: :array, desc: "Subject configuration (can be specified multiple times)"
|
|
47
|
+
option :prompt_base, type: :string, desc: "Base prompt module"
|
|
48
|
+
option :prompt_format, type: :string, desc: "Format module"
|
|
49
|
+
option :prompt_focus, type: :string, desc: "Focus modules (comma-separated)"
|
|
50
|
+
option :add_focus, type: :string, desc: "Add focus modules to preset"
|
|
51
|
+
option :prompt_guidelines, type: :string, desc: "Guideline modules (comma-separated)"
|
|
52
|
+
option :model, type: :array, desc: "LLM model(s) to use (can be specified multiple times)"
|
|
53
|
+
option :no_feedback, type: :boolean, desc: "Skip feedback extraction from review reports"
|
|
54
|
+
option :feedback_model, type: :string, desc: "Model to use for feedback extraction"
|
|
55
|
+
option :dry_run, type: :boolean, desc: "Prepare review without executing"
|
|
56
|
+
option :auto_execute, type: :boolean, default: nil, desc: "Execute LLM query automatically"
|
|
57
|
+
option :save_session, type: :boolean, desc: "Save session files (default: true)"
|
|
58
|
+
option :session_dir, type: :string, desc: "Custom session directory"
|
|
59
|
+
option :pr, type: :string, desc: "Review GitHub PR (number, URL, or owner/repo#number)"
|
|
60
|
+
option :pr_comments, type: :boolean, desc: "Include PR comments as feedback source (default: true for --pr)"
|
|
61
|
+
option :post_comment, type: :boolean, desc: "Post review as PR comment (requires --pr)"
|
|
62
|
+
option :gh_timeout, type: :integer, desc: "Timeout for gh CLI operations in seconds (default: 30)"
|
|
63
|
+
|
|
64
|
+
# Standard options
|
|
65
|
+
option :version, type: :boolean, desc: "Show version information"
|
|
66
|
+
option :list_presets, type: :boolean, desc: "List available review presets"
|
|
67
|
+
option :list_prompts, type: :boolean, desc: "List available prompt modules"
|
|
68
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
69
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
70
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
71
|
+
|
|
72
|
+
attr_reader :options
|
|
73
|
+
|
|
74
|
+
def call(**cli_options)
|
|
75
|
+
# Remove ace-support-cli specific keys (args is leftover arguments)
|
|
76
|
+
cli_options = cli_options.reject { |k, _| k == :args }
|
|
77
|
+
|
|
78
|
+
if cli_options[:version]
|
|
79
|
+
puts "ace-review #{Ace::Review::VERSION}"
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if cli_options[:list_presets]
|
|
84
|
+
show_list_presets
|
|
85
|
+
return
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if cli_options[:list_prompts]
|
|
89
|
+
show_list_prompts
|
|
90
|
+
return
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Type-convert numeric options (ace-support-cli returns strings, Thor converted to integers)
|
|
94
|
+
cli_options[:gh_timeout] = cli_options[:gh_timeout]&.to_i if cli_options[:gh_timeout]
|
|
95
|
+
|
|
96
|
+
# Build and store options for testing compatibility
|
|
97
|
+
@options = build_review_options(cli_options)
|
|
98
|
+
|
|
99
|
+
execute_review
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def execute_review
|
|
105
|
+
display_config_summary
|
|
106
|
+
|
|
107
|
+
# Convert options hash to ReviewOptions object
|
|
108
|
+
review_options = Models::ReviewOptions.new(@options)
|
|
109
|
+
|
|
110
|
+
puts "Analyzing code with preset '#{review_options.preset}'..." if review_options.verbose
|
|
111
|
+
|
|
112
|
+
manager = Organisms::ReviewManager.new
|
|
113
|
+
result = manager.execute_review(review_options)
|
|
114
|
+
|
|
115
|
+
if result[:success]
|
|
116
|
+
handle_success(result, review_options)
|
|
117
|
+
else
|
|
118
|
+
handle_error(result)
|
|
119
|
+
end
|
|
120
|
+
rescue => e
|
|
121
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def build_review_options(cli_options)
|
|
125
|
+
# Start with CLI options
|
|
126
|
+
options = cli_options.dup
|
|
127
|
+
|
|
128
|
+
# Handle repeatable options
|
|
129
|
+
process_subjects(options)
|
|
130
|
+
process_models(options)
|
|
131
|
+
|
|
132
|
+
# Set defaults
|
|
133
|
+
options[:save_session] = true unless options.key?(:save_session)
|
|
134
|
+
|
|
135
|
+
options
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def process_subjects(options)
|
|
139
|
+
return unless options[:subject]
|
|
140
|
+
|
|
141
|
+
# Handle both array input (from tests/API) and string input (from CLI)
|
|
142
|
+
# CLI uses ARRAY_SEPARATOR (\x1F) to preserve internal commas in subjects
|
|
143
|
+
subjects = Array(options[:subject]).flat_map do |s|
|
|
144
|
+
s.to_s.split(CLI::ARRAY_SEPARATOR)
|
|
145
|
+
end.compact.map(&:strip).reject(&:empty?)
|
|
146
|
+
|
|
147
|
+
if subjects.any?
|
|
148
|
+
# Deduplicate subjects (order preserved)
|
|
149
|
+
subjects.uniq!
|
|
150
|
+
# Single value: pass as-is, Multiple: pass as array
|
|
151
|
+
options[:subject] = (subjects.size == 1) ? subjects.first : subjects
|
|
152
|
+
else
|
|
153
|
+
options.delete(:subject)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def process_models(options)
|
|
158
|
+
return unless options[:model]
|
|
159
|
+
|
|
160
|
+
# ace-support-cli's array option gives us an array
|
|
161
|
+
models = Array(options[:model]).compact.map(&:strip).reject(&:empty?)
|
|
162
|
+
|
|
163
|
+
if models.any?
|
|
164
|
+
# Also split comma-separated values
|
|
165
|
+
models = models.flat_map { |m| m.split(",").map(&:strip).reject(&:empty?) }
|
|
166
|
+
# Deduplicate and validate
|
|
167
|
+
models.uniq!
|
|
168
|
+
validate_model_names(models)
|
|
169
|
+
# Store in :models (array) not :model (expects string)
|
|
170
|
+
options[:models] = models
|
|
171
|
+
options.delete(:model)
|
|
172
|
+
else
|
|
173
|
+
options.delete(:model)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def validate_model_names(models)
|
|
178
|
+
models.each do |model|
|
|
179
|
+
unless model.match?(/\A[a-zA-Z0-9\-_:.@]+\z/)
|
|
180
|
+
raise ArgumentError, "Invalid model name '#{model}'. Model names can only contain alphanumeric characters, hyphens, underscores, colons, and dots."
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def handle_success(result, review_options)
|
|
186
|
+
# Handle multi-model results
|
|
187
|
+
if result[:summary]
|
|
188
|
+
handle_multi_model_success(result)
|
|
189
|
+
return
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Display review saved/prepared message
|
|
193
|
+
if result[:output_file]
|
|
194
|
+
puts "✓ Review saved: #{result[:output_file]}"
|
|
195
|
+
elsif result[:session_dir]
|
|
196
|
+
puts "✓ Review session prepared: #{result[:session_dir]}"
|
|
197
|
+
|
|
198
|
+
# Display prompt files for ace-bundle workflow
|
|
199
|
+
if result[:system_prompt_file] && result[:user_prompt_file]
|
|
200
|
+
puts " System prompt: #{result[:system_prompt_file]}"
|
|
201
|
+
puts " User prompt: #{result[:user_prompt_file]}"
|
|
202
|
+
|
|
203
|
+
unless @options[:dry_run]
|
|
204
|
+
puts
|
|
205
|
+
puts "To execute with LLM:"
|
|
206
|
+
puts " ace-llm --file #{result[:user_prompt_file]} --context #{result[:system_prompt_file]}"
|
|
207
|
+
end
|
|
208
|
+
elsif result[:prompt_file]
|
|
209
|
+
puts " Prompt: #{result[:prompt_file]}"
|
|
210
|
+
unless @options[:dry_run]
|
|
211
|
+
puts
|
|
212
|
+
puts "To execute with LLM:"
|
|
213
|
+
puts " ace-llm --file #{result[:prompt_file]}"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Display comment posting results
|
|
219
|
+
if result[:comment_url]
|
|
220
|
+
puts "✓ Review posted to PR: #{result[:comment_url]}"
|
|
221
|
+
elsif result[:comment_error]
|
|
222
|
+
puts "✗ Failed to post comment: #{result[:comment_error]}"
|
|
223
|
+
puts " (Review saved locally - you can post manually)"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Display dry-run preview
|
|
227
|
+
if result[:dry_run_preview]
|
|
228
|
+
puts
|
|
229
|
+
puts "=== Comment Preview (Dry Run) ==="
|
|
230
|
+
puts result[:dry_run_preview]
|
|
231
|
+
puts "=== End Preview ==="
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def handle_multi_model_success(result)
|
|
236
|
+
puts
|
|
237
|
+
puts "Reviews saved (#{result[:summary][:success_count]} of #{result[:summary][:total_models]} succeeded):"
|
|
238
|
+
puts " Session directory: #{result[:session_dir]}"
|
|
239
|
+
puts
|
|
240
|
+
|
|
241
|
+
if result[:output_files]&.any?
|
|
242
|
+
result[:output_files].each do |file|
|
|
243
|
+
puts " ✓ #{File.basename(file)}"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
if result[:failed_models]&.any?
|
|
248
|
+
puts
|
|
249
|
+
puts "Failed models:"
|
|
250
|
+
result[:failed_models].each do |model|
|
|
251
|
+
puts " ✗ #{model}"
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
puts
|
|
256
|
+
puts "Total duration: #{result[:summary][:total_duration]}s"
|
|
257
|
+
|
|
258
|
+
if result[:feedback_count]
|
|
259
|
+
puts
|
|
260
|
+
puts "Feedback: #{result[:feedback_count]} items extracted"
|
|
261
|
+
elsif result[:feedback_error]
|
|
262
|
+
puts
|
|
263
|
+
puts "Feedback extraction failed: #{result[:feedback_error]}"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def handle_error(result)
|
|
268
|
+
raise Ace::Support::Cli::Error.new(result[:error])
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def display_config_summary
|
|
272
|
+
return if @options[:quiet]
|
|
273
|
+
|
|
274
|
+
require "ace/core"
|
|
275
|
+
Ace::Core::Atoms::ConfigSummary.display(
|
|
276
|
+
command: "review",
|
|
277
|
+
config: Ace::Review.config,
|
|
278
|
+
defaults: load_defaults,
|
|
279
|
+
options: @options,
|
|
280
|
+
quiet: false
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def show_list_presets
|
|
285
|
+
manager = Organisms::ReviewManager.new
|
|
286
|
+
|
|
287
|
+
presets = manager.list_presets
|
|
288
|
+
if presets.empty?
|
|
289
|
+
puts "No presets found"
|
|
290
|
+
puts "Create presets in .ace/review/config.yml or .ace/review/presets/"
|
|
291
|
+
return
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
puts "Available Review Presets:"
|
|
295
|
+
puts
|
|
296
|
+
|
|
297
|
+
# Header
|
|
298
|
+
puts format("%-20s %-50s %-10s", "Preset", "Description", "Source")
|
|
299
|
+
puts "-" * 80
|
|
300
|
+
|
|
301
|
+
# Load preset manager to get descriptions
|
|
302
|
+
preset_manager = Molecules::PresetManager.new
|
|
303
|
+
|
|
304
|
+
presets.each do |name|
|
|
305
|
+
preset = preset_manager.load_preset(name)
|
|
306
|
+
description = preset&.dig("description") || "-"
|
|
307
|
+
|
|
308
|
+
# Determine source
|
|
309
|
+
source = if preset_manager.send(:load_preset_from_file, name)
|
|
310
|
+
"file"
|
|
311
|
+
elsif preset_manager.send(:load_preset_from_config, name)
|
|
312
|
+
"config"
|
|
313
|
+
else
|
|
314
|
+
"default"
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
puts format("%-20s %-50s %-10s", name, description, source)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def show_list_prompts
|
|
322
|
+
manager = Organisms::ReviewManager.new
|
|
323
|
+
|
|
324
|
+
prompts = manager.list_prompts
|
|
325
|
+
if prompts.empty?
|
|
326
|
+
puts "No prompt modules found"
|
|
327
|
+
return
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
puts "Available Prompt Modules:"
|
|
331
|
+
puts
|
|
332
|
+
|
|
333
|
+
prompts.each do |category, items|
|
|
334
|
+
puts " #{category}/"
|
|
335
|
+
format_prompt_items(items, " ")
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def format_prompt_items(items, indent)
|
|
340
|
+
case items
|
|
341
|
+
when Hash
|
|
342
|
+
items.each do |name, value|
|
|
343
|
+
if value.is_a?(Array)
|
|
344
|
+
puts "#{indent}#{name}/"
|
|
345
|
+
value.each do |item|
|
|
346
|
+
source = item.is_a?(Hash) ? " (#{item[:source]})" : ""
|
|
347
|
+
item_name = item.is_a?(Hash) ? item[:name] : item
|
|
348
|
+
puts "#{indent} #{item_name}#{source}"
|
|
349
|
+
end
|
|
350
|
+
else
|
|
351
|
+
source = value.is_a?(String) ? " (#{value})" : ""
|
|
352
|
+
puts "#{indent}#{name}#{source}"
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
when Array
|
|
356
|
+
items.each { |item| puts "#{indent}#{item}" }
|
|
357
|
+
when String
|
|
358
|
+
puts "#{indent}#{items}"
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def load_defaults
|
|
363
|
+
gem_root = Gem.loaded_specs["ace-review"]&.gem_dir ||
|
|
364
|
+
File.expand_path("../../../../../..", __dir__)
|
|
365
|
+
defaults_path = File.join(gem_root, ".ace-defaults", "review", "config.yml")
|
|
366
|
+
|
|
367
|
+
if File.exist?(defaults_path)
|
|
368
|
+
require "yaml"
|
|
369
|
+
YAML.safe_load_file(defaults_path, permitted_classes: [Date], aliases: true) || {}
|
|
370
|
+
else
|
|
371
|
+
{}
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
require_relative "../version"
|
|
6
|
+
|
|
7
|
+
# Reuse existing feedback command classes
|
|
8
|
+
require_relative "commands/feedback/create"
|
|
9
|
+
require_relative "commands/feedback/list"
|
|
10
|
+
require_relative "commands/feedback/show"
|
|
11
|
+
require_relative "commands/feedback/verify"
|
|
12
|
+
require_relative "commands/feedback/skip"
|
|
13
|
+
require_relative "commands/feedback/resolve"
|
|
14
|
+
|
|
15
|
+
module Ace
|
|
16
|
+
module Review
|
|
17
|
+
# Flat CLI registry for ace-review-feedback (review feedback management).
|
|
18
|
+
#
|
|
19
|
+
# Replaces the nested `ace-review feedback <subcommand>` pattern with
|
|
20
|
+
# flat `ace-review-feedback <command>` invocations.
|
|
21
|
+
module FeedbackCLI
|
|
22
|
+
extend Ace::Support::Cli::RegistryDsl
|
|
23
|
+
|
|
24
|
+
PROGRAM_NAME = "ace-review-feedback"
|
|
25
|
+
|
|
26
|
+
# Application commands with descriptions (for help output)
|
|
27
|
+
REGISTERED_COMMANDS = [
|
|
28
|
+
["list", "List feedback items from a review"],
|
|
29
|
+
["create", "Create feedback from review output"],
|
|
30
|
+
["show", "Show feedback details"],
|
|
31
|
+
["verify", "Verify feedback as valid or invalid"],
|
|
32
|
+
["skip", "Skip a feedback item"],
|
|
33
|
+
["resolve", "Resolve a feedback item"]
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
HELP_EXAMPLES = [
|
|
37
|
+
"ace-review-feedback create # From latest review session",
|
|
38
|
+
"ace-review-feedback list --status pending # Unresolved items",
|
|
39
|
+
"ace-review-feedback verify 42 --valid # Confirm finding",
|
|
40
|
+
"ace-review-feedback resolve 42 # Mark as resolved"
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
# Register flat commands (reusing existing command classes)
|
|
44
|
+
register "list", CLI::Commands::FeedbackSubcommands::List
|
|
45
|
+
register "create", CLI::Commands::FeedbackSubcommands::Create
|
|
46
|
+
register "show", CLI::Commands::FeedbackSubcommands::Show
|
|
47
|
+
register "verify", CLI::Commands::FeedbackSubcommands::Verify
|
|
48
|
+
register "skip", CLI::Commands::FeedbackSubcommands::Skip
|
|
49
|
+
register "resolve", CLI::Commands::FeedbackSubcommands::Resolve
|
|
50
|
+
|
|
51
|
+
# Register version command
|
|
52
|
+
version_cmd = Ace::Support::Cli::VersionCommand.build(
|
|
53
|
+
gem_name: "ace-review-feedback",
|
|
54
|
+
version: Ace::Review::VERSION
|
|
55
|
+
)
|
|
56
|
+
register "version", version_cmd
|
|
57
|
+
register "--version", version_cmd
|
|
58
|
+
|
|
59
|
+
# Register help command
|
|
60
|
+
help_cmd = Ace::Support::Cli::HelpCommand.build(
|
|
61
|
+
program_name: PROGRAM_NAME,
|
|
62
|
+
version: Ace::Review::VERSION,
|
|
63
|
+
commands: REGISTERED_COMMANDS,
|
|
64
|
+
examples: HELP_EXAMPLES
|
|
65
|
+
)
|
|
66
|
+
register "help", help_cmd
|
|
67
|
+
register "--help", help_cmd
|
|
68
|
+
register "-h", help_cmd
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
require_relative "../review"
|
|
6
|
+
# Commands
|
|
7
|
+
require_relative "cli/commands/review"
|
|
8
|
+
|
|
9
|
+
module Ace
|
|
10
|
+
module Review
|
|
11
|
+
# CLI namespace for ace-review command loading.
|
|
12
|
+
#
|
|
13
|
+
# ace-review uses a single-command ace-support-cli entrypoint that calls
|
|
14
|
+
# CLI::Commands::Review directly from the executable.
|
|
15
|
+
module CLI
|
|
16
|
+
# Separator for array options that won't conflict with internal commas
|
|
17
|
+
# ASCII Unit Separator (0x1F) is designed for separating fields
|
|
18
|
+
ARRAY_SEPARATOR = "\x1F"
|
|
19
|
+
|
|
20
|
+
# Pre-process array options to work around ace-support-cli limitation
|
|
21
|
+
#
|
|
22
|
+
# ace-support-cli's type: :array only captures the last occurrence of a flag.
|
|
23
|
+
# This method merges multiple occurrences using ARRAY_SEPARATOR
|
|
24
|
+
# (not comma) to preserve internal commas in subject values
|
|
25
|
+
# like "files:a.rb,b.rb".
|
|
26
|
+
#
|
|
27
|
+
# @param args [Array<String>] Raw command-line arguments
|
|
28
|
+
# @return [Array<String>] Pre-processed arguments with merged array options
|
|
29
|
+
def self.preprocess_array_options(args)
|
|
30
|
+
result = []
|
|
31
|
+
i = 0
|
|
32
|
+
accumulated_subject = []
|
|
33
|
+
accumulated_model = []
|
|
34
|
+
|
|
35
|
+
while i < args.length
|
|
36
|
+
arg = args[i]
|
|
37
|
+
|
|
38
|
+
# Track --subject occurrences for merging
|
|
39
|
+
if arg == "--subject" || arg.start_with?("--subject=")
|
|
40
|
+
value = extract_flag_value(arg, args, i)
|
|
41
|
+
accumulated_subject << value
|
|
42
|
+
i = skip_to_next_arg(args, i)
|
|
43
|
+
next
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Track --model occurrences for merging
|
|
47
|
+
if arg == "--model" || arg.start_with?("--model=")
|
|
48
|
+
value = extract_flag_value(arg, args, i)
|
|
49
|
+
accumulated_model << value
|
|
50
|
+
i = skip_to_next_arg(args, i)
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
result << arg
|
|
55
|
+
i += 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Insert merged flags at position 0 (single-command mode, no command name to skip)
|
|
59
|
+
result.insert(0, "--model", accumulated_model.join(",")) unless accumulated_model.empty?
|
|
60
|
+
# Subject uses ARRAY_SEPARATOR to preserve internal commas (e.g., files:a.rb,b.rb)
|
|
61
|
+
result.insert(0, "--subject", accumulated_subject.join(ARRAY_SEPARATOR)) unless accumulated_subject.empty?
|
|
62
|
+
|
|
63
|
+
result
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Extract value from flag argument
|
|
67
|
+
#
|
|
68
|
+
# @param arg [String] Current argument (may be --flag=value or just --flag)
|
|
69
|
+
# @param args [Array<String>] All arguments
|
|
70
|
+
# @param index [Integer] Current index
|
|
71
|
+
# @return [String] Extracted value
|
|
72
|
+
def self.extract_flag_value(arg, args, index)
|
|
73
|
+
if arg.include?("=")
|
|
74
|
+
arg.split("=", 2)[1]
|
|
75
|
+
elsif index + 1 < args.length && !args[index + 1].start_with?("--")
|
|
76
|
+
args[index + 1]
|
|
77
|
+
else
|
|
78
|
+
""
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Calculate next index after consuming flag value
|
|
83
|
+
#
|
|
84
|
+
# @param args [Array<String>] All arguments
|
|
85
|
+
# @param index [Integer] Current index
|
|
86
|
+
# @return [Integer] Next index to process
|
|
87
|
+
def self.skip_to_next_arg(args, index)
|
|
88
|
+
if args[index].include?("=") || (index + 1 < args.length && !args[index + 1].start_with?("--"))
|
|
89
|
+
index + 2
|
|
90
|
+
else
|
|
91
|
+
index + 1
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Entry point for CLI invocation (used by tests via cli_helpers)
|
|
96
|
+
#
|
|
97
|
+
# @param args [Array<String>] Command-line arguments
|
|
98
|
+
def self.start(args)
|
|
99
|
+
Ace::Support::Cli::Runner.new(Commands::Review).call(args: args)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Review
|
|
5
|
+
# Namespace for ace-review errors
|
|
6
|
+
module Errors
|
|
7
|
+
# Base error class for all ace-review errors
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Raised when a required dependency is missing
|
|
11
|
+
class MissingDependencyError < Error
|
|
12
|
+
attr_reader :dependency_name, :install_command
|
|
13
|
+
|
|
14
|
+
def initialize(dependency_name, install_command = nil)
|
|
15
|
+
@dependency_name = dependency_name
|
|
16
|
+
@install_command = install_command || "gem install #{dependency_name}"
|
|
17
|
+
|
|
18
|
+
message = "Required gem '#{dependency_name}' not found.\n"
|
|
19
|
+
message += "Install with: #{@install_command}"
|
|
20
|
+
|
|
21
|
+
super(message)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Raised when ace-bundle fails to process bundle
|
|
26
|
+
class BundleProcessingError < Error
|
|
27
|
+
attr_reader :details
|
|
28
|
+
|
|
29
|
+
def initialize(message, details = nil)
|
|
30
|
+
@details = details
|
|
31
|
+
super(message)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Raised when gh CLI is not installed
|
|
36
|
+
class GhCliNotInstalledError < Error
|
|
37
|
+
def initialize
|
|
38
|
+
message = "GitHub CLI (gh) is not installed.\n"
|
|
39
|
+
message += "Install with:\n"
|
|
40
|
+
message += " macOS: brew install gh\n"
|
|
41
|
+
message += " Linux: See https://cli.github.com/manual/installation\n"
|
|
42
|
+
message += " Windows: See https://cli.github.com/manual/installation"
|
|
43
|
+
|
|
44
|
+
super(message)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Raised when user is not authenticated with GitHub
|
|
49
|
+
class GhAuthenticationError < Error
|
|
50
|
+
def initialize
|
|
51
|
+
message = "GitHub authentication required.\n"
|
|
52
|
+
message += "Run 'gh auth login' to authenticate with GitHub.\n"
|
|
53
|
+
message += "Check status: gh auth status"
|
|
54
|
+
|
|
55
|
+
super(message)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Raised when a PR is not found
|
|
60
|
+
class PrNotFoundError < Error
|
|
61
|
+
attr_reader :pr_identifier
|
|
62
|
+
|
|
63
|
+
def initialize(pr_identifier, details = nil)
|
|
64
|
+
@pr_identifier = pr_identifier
|
|
65
|
+
message = "Pull request '#{pr_identifier}' not found."
|
|
66
|
+
message += "\n#{details}" if details
|
|
67
|
+
|
|
68
|
+
super(message)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Raised when PR diff exceeds GitHub's API size limit (300 files)
|
|
73
|
+
class DiffTooLargeError < Error
|
|
74
|
+
attr_reader :pr_identifier
|
|
75
|
+
|
|
76
|
+
def initialize(pr_identifier, details = nil)
|
|
77
|
+
@pr_identifier = pr_identifier
|
|
78
|
+
message = "Pull request '#{pr_identifier}' diff exceeds GitHub API size limit."
|
|
79
|
+
message += "\n#{details}" if details
|
|
80
|
+
|
|
81
|
+
super(message)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Raised when attempting to post to a closed/merged PR
|
|
86
|
+
class PrStateError < Error
|
|
87
|
+
attr_reader :pr_number, :state
|
|
88
|
+
|
|
89
|
+
def initialize(pr_number, state)
|
|
90
|
+
@pr_number = pr_number
|
|
91
|
+
@state = state
|
|
92
|
+
|
|
93
|
+
message = "Cannot post comment to PR ##{pr_number}.\n"
|
|
94
|
+
message += "PR is in '#{state}' state. Comments can only be posted to open PRs."
|
|
95
|
+
|
|
96
|
+
super(message)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Raised when gh CLI encounters a network error
|
|
101
|
+
class GhNetworkError < Error; end
|
|
102
|
+
|
|
103
|
+
# Raised when ace-task cannot find task
|
|
104
|
+
class TaskNotFoundError < Error
|
|
105
|
+
attr_reader :task_ref
|
|
106
|
+
|
|
107
|
+
def initialize(task_ref)
|
|
108
|
+
@task_ref = task_ref
|
|
109
|
+
message = "Task '#{task_ref}' not found.\n"
|
|
110
|
+
message += "Run 'ace-task show #{task_ref}' for details."
|
|
111
|
+
super(message)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Raised when task exists but has no path
|
|
116
|
+
class TaskPathNotFoundError < Error
|
|
117
|
+
attr_reader :task_ref
|
|
118
|
+
|
|
119
|
+
def initialize(task_ref)
|
|
120
|
+
@task_ref = task_ref
|
|
121
|
+
message = "Task '#{task_ref}' exists but has no path.\n"
|
|
122
|
+
message += "Check task status with: ace-task show #{task_ref}"
|
|
123
|
+
super(message)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Raised when a subprocess command times out
|
|
128
|
+
class CommandTimeoutError < Error
|
|
129
|
+
attr_reader :command, :timeout_seconds
|
|
130
|
+
|
|
131
|
+
def initialize(command, timeout_seconds)
|
|
132
|
+
@command = command
|
|
133
|
+
@timeout_seconds = timeout_seconds
|
|
134
|
+
message = "Command '#{command}' timed out after #{timeout_seconds} seconds."
|
|
135
|
+
super(message)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Raised when context composition fails
|
|
140
|
+
class ContextComposerError < Error; end
|
|
141
|
+
|
|
142
|
+
# Raised when an unknown review strategy is requested
|
|
143
|
+
class UnknownStrategyError < Error; end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|