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,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
|