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,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+ require "tempfile"
6
+
7
+ module Ace
8
+ module Review
9
+ module Molecules
10
+ # Writes FeedbackItem instances to disk as markdown files with YAML frontmatter.
11
+ #
12
+ # Files are written atomically (temp file + rename) with file locking
13
+ # to ensure safe concurrent access.
14
+ #
15
+ # @example Write a feedback item
16
+ # writer = FeedbackFileWriter.new
17
+ # result = writer.write(feedback_item, "/path/to/feedback")
18
+ # result[:success] #=> true
19
+ # result[:path] #=> "/path/to/feedback/8o7abc-missing-error-handling.s.md"
20
+ #
21
+ class FeedbackFileWriter
22
+ # Write a FeedbackItem to disk
23
+ #
24
+ # @param feedback_item [Models::FeedbackItem] The feedback item to write
25
+ # @param directory [String] The directory to write to
26
+ # @return [Hash] Result hash with :success, :path or :error keys
27
+ def write(feedback_item, directory)
28
+ validate_inputs(feedback_item, directory)
29
+
30
+ filename = generate_filename(feedback_item)
31
+ file_path = File.join(directory, filename)
32
+ content = generate_content(feedback_item)
33
+
34
+ write_atomic(file_path, content)
35
+ rescue ArgumentError => e
36
+ {success: false, error: e.message}
37
+ rescue SystemCallError, IOError => e
38
+ {success: false, error: "Failed to write feedback file: #{e.message}"}
39
+ end
40
+
41
+ private
42
+
43
+ # Validate inputs before writing
44
+ #
45
+ # @param feedback_item [Models::FeedbackItem] The feedback item
46
+ # @param directory [String] The directory path
47
+ # @raise [ArgumentError] If inputs are invalid
48
+ def validate_inputs(feedback_item, directory)
49
+ raise ArgumentError, "feedback_item is required" if feedback_item.nil?
50
+ raise ArgumentError, "directory is required" if directory.nil? || directory.empty?
51
+ raise ArgumentError, "directory does not exist: #{directory}" unless Dir.exist?(directory)
52
+
53
+ unless feedback_item.is_a?(Models::FeedbackItem)
54
+ raise ArgumentError, "feedback_item must be a FeedbackItem"
55
+ end
56
+
57
+ raise ArgumentError, "feedback_item.id is required" if feedback_item.id.nil? || feedback_item.id.empty?
58
+ raise ArgumentError, "feedback_item.title is required" if feedback_item.title.nil? || feedback_item.title.empty?
59
+ end
60
+
61
+ # Generate the filename for a feedback item
62
+ #
63
+ # @param feedback_item [Models::FeedbackItem] The feedback item
64
+ # @return [String] The filename in format: {id}-{slug}.s.md
65
+ def generate_filename(feedback_item)
66
+ slug = Atoms::FeedbackSlugGenerator.generate(feedback_item.title)
67
+ "#{feedback_item.id}-#{slug}.s.md"
68
+ end
69
+
70
+ # Generate the file content with YAML frontmatter and markdown sections
71
+ #
72
+ # @param feedback_item [Models::FeedbackItem] The feedback item
73
+ # @return [String] The complete file content
74
+ def generate_content(feedback_item)
75
+ frontmatter = generate_frontmatter(feedback_item)
76
+ sections = generate_sections(feedback_item)
77
+
78
+ "#{frontmatter}#{sections}"
79
+ end
80
+
81
+ # Generate YAML frontmatter
82
+ #
83
+ # @param feedback_item [Models::FeedbackItem] The feedback item
84
+ # @return [String] YAML frontmatter with delimiters
85
+ def generate_frontmatter(feedback_item)
86
+ data = {
87
+ "id" => feedback_item.id,
88
+ "title" => feedback_item.title,
89
+ "files" => feedback_item.files,
90
+ "status" => feedback_item.status,
91
+ "priority" => feedback_item.priority,
92
+ "created" => feedback_item.created,
93
+ "updated" => feedback_item.updated
94
+ }
95
+
96
+ # Use reviewers array if multiple reviewers, otherwise use legacy reviewer key
97
+ if feedback_item.reviewers.length > 1
98
+ data["reviewers"] = feedback_item.reviewers.dup
99
+ data["consensus"] = feedback_item.consensus if feedback_item.consensus
100
+ else
101
+ data["reviewer"] = feedback_item.reviewer
102
+ end
103
+
104
+ data = data.compact
105
+
106
+ "---\n#{data.to_yaml.sub(/\A---\n/, "")}---\n\n"
107
+ end
108
+
109
+ # Generate markdown sections
110
+ #
111
+ # @param feedback_item [Models::FeedbackItem] The feedback item
112
+ # @return [String] Markdown sections
113
+ def generate_sections(feedback_item)
114
+ sections = []
115
+
116
+ sections << "## Finding\n#{feedback_item.finding}" if feedback_item.finding
117
+ sections << "## Context\n#{feedback_item.context}" if feedback_item.context
118
+ sections << "## Research\n#{feedback_item.research}" if feedback_item.research
119
+ sections << "## Resolution\n#{feedback_item.resolution}" if feedback_item.resolution
120
+
121
+ sections.join("\n\n")
122
+ end
123
+
124
+ # Write content atomically with file locking and retries
125
+ #
126
+ # Uses a dedicated lock file in the target directory to ensure
127
+ # concurrent processes properly coordinate writes. Lock file is
128
+ # cleaned up after successful write.
129
+ #
130
+ # @param file_path [String] The target file path
131
+ # @param content [String] The content to write
132
+ # @return [Hash] Result hash with :success and :path or :error
133
+ def write_atomic(file_path, content)
134
+ dir = File.dirname(file_path)
135
+ lock_file_path = File.join(dir, ".feedback.lock")
136
+
137
+ temp_file = nil
138
+ result = nil
139
+
140
+ # Open lock file (create if needed)
141
+ File.open(lock_file_path, File::RDWR | File::CREAT) do |lock_file|
142
+ # Acquire exclusive lock with retry
143
+ unless acquire_lock(lock_file)
144
+ return {success: false, error: "Could not acquire file lock"}
145
+ end
146
+
147
+ begin
148
+ # Create temp file and write
149
+ temp_file = Tempfile.new(["feedback", ".s.md"], dir)
150
+ temp_file.write(content)
151
+ temp_file.flush
152
+ temp_file.fsync
153
+ temp_path = temp_file.path
154
+ temp_file.close
155
+
156
+ # Atomic rename
157
+ File.rename(temp_path, file_path)
158
+
159
+ result = {success: true, path: file_path}
160
+ ensure
161
+ temp_file.close! if temp_file && !temp_file.closed?
162
+ end
163
+ end
164
+
165
+ result
166
+ rescue Errno::EAGAIN, Errno::EACCES => e
167
+ {success: false, error: "File lock timeout: #{e.message}"}
168
+ ensure
169
+ # Clean up lock file after write completes
170
+ begin
171
+ FileUtils.rm_f(lock_file_path)
172
+ rescue => e
173
+ warn "Failed to clean up lock file #{lock_file_path}: #{e.message}"
174
+ end
175
+ end
176
+
177
+ # Attempt to acquire an exclusive file lock
178
+ #
179
+ # Uses non-blocking lock with retry logic and exponential backoff.
180
+ # This prevents indefinite waiting while allowing for transient
181
+ # lock contention. Falls back to blocking lock if retries exhausted.
182
+ #
183
+ # @param lock_file [File] The lock file to lock
184
+ # @param max_attempts [Integer] Maximum number of non-blocking attempts
185
+ # @return [Boolean] True if lock acquired
186
+ def acquire_lock(lock_file, max_attempts: 5)
187
+ max_attempts.times do |attempt|
188
+ # Try non-blocking lock first
189
+ result = lock_file.flock(File::LOCK_EX | File::LOCK_NB)
190
+ return true if result == 0
191
+
192
+ # If not acquired and not the last attempt, wait with exponential backoff
193
+ break if attempt == max_attempts - 1
194
+
195
+ # Exponential backoff: 0.01s, 0.02s, 0.04s, 0.08s, 0.16s
196
+ sleep_time = 0.01 * (2**attempt)
197
+ sleep(sleep_time)
198
+ end
199
+
200
+ # Fall back to blocking lock if all non-blocking attempts failed
201
+ # This ensures we eventually acquire the lock, just may wait longer
202
+ lock_file.flock(File::LOCK_EX)
203
+ true
204
+ rescue Errno::EAGAIN, Errno::EACCES, Errno::EINTR
205
+ false
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end