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.
Files changed (108) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/nav/protocols/guide-sources/ace-review.yml +10 -0
  3. data/.ace-defaults/nav/protocols/prompt-sources/ace-review.yml +36 -0
  4. data/.ace-defaults/nav/protocols/tmpl-sources/ace-review.yml +10 -0
  5. data/.ace-defaults/nav/protocols/wfi-sources/ace-review.yml +19 -0
  6. data/.ace-defaults/review/config.yml +79 -0
  7. data/.ace-defaults/review/presets/code-fit.yml +64 -0
  8. data/.ace-defaults/review/presets/code-shine.yml +44 -0
  9. data/.ace-defaults/review/presets/code-valid.yml +39 -0
  10. data/.ace-defaults/review/presets/docs.yml +42 -0
  11. data/.ace-defaults/review/presets/spec.yml +37 -0
  12. data/CHANGELOG.md +1780 -0
  13. data/LICENSE +21 -0
  14. data/README.md +42 -0
  15. data/Rakefile +14 -0
  16. data/exe/ace-review +27 -0
  17. data/exe/ace-review-feedback +17 -0
  18. data/handbook/guides/code-review-process.g.md +234 -0
  19. data/handbook/prompts/base/sections.md +23 -0
  20. data/handbook/prompts/base/system.md +60 -0
  21. data/handbook/prompts/focus/architecture/atom.md +30 -0
  22. data/handbook/prompts/focus/architecture/reflection.md +60 -0
  23. data/handbook/prompts/focus/frameworks/rails.md +40 -0
  24. data/handbook/prompts/focus/frameworks/vue-firebase.md +45 -0
  25. data/handbook/prompts/focus/languages/ruby.md +50 -0
  26. data/handbook/prompts/focus/phase/correctness.md +51 -0
  27. data/handbook/prompts/focus/phase/polish.md +43 -0
  28. data/handbook/prompts/focus/phase/quality.md +42 -0
  29. data/handbook/prompts/focus/quality/performance.md +48 -0
  30. data/handbook/prompts/focus/quality/security.md +47 -0
  31. data/handbook/prompts/focus/scope/docs.md +38 -0
  32. data/handbook/prompts/focus/scope/spec.md +58 -0
  33. data/handbook/prompts/focus/scope/tests.md +36 -0
  34. data/handbook/prompts/format/compact.md +12 -0
  35. data/handbook/prompts/format/detailed.md +39 -0
  36. data/handbook/prompts/format/standard.md +16 -0
  37. data/handbook/prompts/guidelines/icons.md +19 -0
  38. data/handbook/prompts/guidelines/tone.md +21 -0
  39. data/handbook/prompts/synthesis-review-reports.system.md +318 -0
  40. data/handbook/prompts/synthesize-feedback.system.md +147 -0
  41. data/handbook/skills/as-review-apply-feedback/SKILL.md +39 -0
  42. data/handbook/skills/as-review-package/SKILL.md +36 -0
  43. data/handbook/skills/as-review-pr/SKILL.md +38 -0
  44. data/handbook/skills/as-review-run/SKILL.md +30 -0
  45. data/handbook/skills/as-review-verify-feedback/SKILL.md +31 -0
  46. data/handbook/templates/review-tasks/task-review-summary.template.md +148 -0
  47. data/handbook/workflow-instructions/review/apply-feedback.wf.md +212 -0
  48. data/handbook/workflow-instructions/review/package.wf.md +16 -0
  49. data/handbook/workflow-instructions/review/pr.wf.md +284 -0
  50. data/handbook/workflow-instructions/review/run.wf.md +262 -0
  51. data/handbook/workflow-instructions/review/verify-feedback.wf.md +286 -0
  52. data/lib/ace/review/atoms/context_limit_resolver.rb +162 -0
  53. data/lib/ace/review/atoms/diff_boundary_finder.rb +133 -0
  54. data/lib/ace/review/atoms/feedback_id_generator.rb +66 -0
  55. data/lib/ace/review/atoms/feedback_slug_generator.rb +61 -0
  56. data/lib/ace/review/atoms/feedback_state_validator.rb +98 -0
  57. data/lib/ace/review/atoms/pr_comment_formatter.rb +325 -0
  58. data/lib/ace/review/atoms/preset_validator.rb +103 -0
  59. data/lib/ace/review/atoms/priority_filter.rb +115 -0
  60. data/lib/ace/review/atoms/retry_with_backoff.rb +75 -0
  61. data/lib/ace/review/atoms/slug_generator.rb +50 -0
  62. data/lib/ace/review/atoms/token_estimator.rb +86 -0
  63. data/lib/ace/review/cli/commands/feedback/create.rb +173 -0
  64. data/lib/ace/review/cli/commands/feedback/list.rb +280 -0
  65. data/lib/ace/review/cli/commands/feedback/resolve.rb +109 -0
  66. data/lib/ace/review/cli/commands/feedback/session_discovery.rb +70 -0
  67. data/lib/ace/review/cli/commands/feedback/show.rb +177 -0
  68. data/lib/ace/review/cli/commands/feedback/skip.rb +125 -0
  69. data/lib/ace/review/cli/commands/feedback/verify.rb +149 -0
  70. data/lib/ace/review/cli/commands/feedback.rb +79 -0
  71. data/lib/ace/review/cli/commands/review.rb +378 -0
  72. data/lib/ace/review/cli/feedback_cli.rb +71 -0
  73. data/lib/ace/review/cli.rb +103 -0
  74. data/lib/ace/review/errors.rb +146 -0
  75. data/lib/ace/review/models/feedback_item.rb +216 -0
  76. data/lib/ace/review/models/review_options.rb +208 -0
  77. data/lib/ace/review/models/reviewer.rb +181 -0
  78. data/lib/ace/review/molecules/context_composer.rb +123 -0
  79. data/lib/ace/review/molecules/context_extractor.rb +159 -0
  80. data/lib/ace/review/molecules/feedback_directory_manager.rb +183 -0
  81. data/lib/ace/review/molecules/feedback_file_reader.rb +178 -0
  82. data/lib/ace/review/molecules/feedback_file_writer.rb +210 -0
  83. data/lib/ace/review/molecules/feedback_synthesizer.rb +588 -0
  84. data/lib/ace/review/molecules/gh_cli_executor.rb +124 -0
  85. data/lib/ace/review/molecules/gh_comment_poster.rb +205 -0
  86. data/lib/ace/review/molecules/gh_comment_resolver.rb +199 -0
  87. data/lib/ace/review/molecules/gh_pr_comment_fetcher.rb +408 -0
  88. data/lib/ace/review/molecules/gh_pr_fetcher.rb +240 -0
  89. data/lib/ace/review/molecules/llm_executor.rb +142 -0
  90. data/lib/ace/review/molecules/multi_model_executor.rb +278 -0
  91. data/lib/ace/review/molecules/nav_prompt_resolver.rb +145 -0
  92. data/lib/ace/review/molecules/pr_task_spec_resolver.rb +58 -0
  93. data/lib/ace/review/molecules/preset_manager.rb +494 -0
  94. data/lib/ace/review/molecules/prompt_composer.rb +76 -0
  95. data/lib/ace/review/molecules/prompt_resolver.rb +168 -0
  96. data/lib/ace/review/molecules/strategies/adaptive_strategy.rb +193 -0
  97. data/lib/ace/review/molecules/strategies/chunked_strategy.rb +459 -0
  98. data/lib/ace/review/molecules/strategies/full_strategy.rb +114 -0
  99. data/lib/ace/review/molecules/subject_extractor.rb +315 -0
  100. data/lib/ace/review/molecules/subject_filter.rb +199 -0
  101. data/lib/ace/review/molecules/subject_strategy.rb +96 -0
  102. data/lib/ace/review/molecules/task_report_saver.rb +161 -0
  103. data/lib/ace/review/molecules/task_resolver.rb +48 -0
  104. data/lib/ace/review/organisms/feedback_manager.rb +386 -0
  105. data/lib/ace/review/organisms/review_manager.rb +1059 -0
  106. data/lib/ace/review/version.rb +7 -0
  107. data/lib/ace/review.rb +135 -0
  108. 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