brainiac-basecamp 0.0.16 → 0.0.18
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 +4 -4
- data/lib/brainiac/plugins/basecamp/cli.rb +534 -10
- data/lib/brainiac/plugins/basecamp/comment_responder.rb +380 -0
- data/lib/brainiac/plugins/basecamp/epic_memory.rb +10 -0
- data/lib/brainiac/plugins/basecamp/hooks.rb +94 -270
- data/lib/brainiac/plugins/basecamp/orchestrator.rb +65 -187
- data/lib/brainiac/plugins/basecamp/review_gate.rb +165 -218
- data/lib/brainiac/plugins/basecamp/session_registry.rb +361 -0
- data/lib/brainiac/plugins/basecamp/task_state.rb +86 -0
- data/lib/brainiac/plugins/basecamp/version.rb +1 -1
- data/lib/brainiac/plugins/basecamp/webhook.rb +4 -6
- data/lib/brainiac/plugins/basecamp.rb +128 -492
- metadata +4 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
|
+
require "time"
|
|
4
5
|
|
|
5
6
|
module Brainiac
|
|
6
7
|
module Plugins
|
|
@@ -29,6 +30,7 @@ module Brainiac
|
|
|
29
30
|
# Gates can also be triggered by a Fizzy card tag "review-gates" for non-epic PRs.
|
|
30
31
|
module ReviewGate
|
|
31
32
|
BRAINIAC_DIR = ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac"))
|
|
33
|
+
RESPONDED_STATUSES = %w[approved changes_requested].freeze
|
|
32
34
|
|
|
33
35
|
class << self
|
|
34
36
|
# Get the configured review gates as normalized hashes.
|
|
@@ -64,11 +66,38 @@ module Brainiac
|
|
|
64
66
|
def all_gates_passed?(task)
|
|
65
67
|
return true unless enabled?
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
gate_states(task).values.all? { |state| state["status"] == "approved" }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def all_gates_responded?(task)
|
|
73
|
+
return true unless enabled?
|
|
74
|
+
|
|
75
|
+
gate_states(task).values.all? { |state| RESPONDED_STATUSES.include?(state["status"]) }
|
|
76
|
+
end
|
|
70
77
|
|
|
71
|
-
|
|
78
|
+
def changes_requested?(task)
|
|
79
|
+
gate_states(task).values.any? { |state| state["status"] == "changes_requested" }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def responded_count(task)
|
|
83
|
+
gate_states(task).values.count { |state| RESPONDED_STATUSES.include?(state["status"]) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Each configured gate gets one durable state record, keyed by agent name.
|
|
87
|
+
# This also migrates persisted state from releases that used flat arrays.
|
|
88
|
+
#
|
|
89
|
+
# NOTE: This method intentionally MUTATES the task hash on read — it lazily
|
|
90
|
+
# initializes gate entries for any newly-configured gates. This means adding
|
|
91
|
+
# a gate to config and calling gate_states on a persisted task will insert a
|
|
92
|
+
# new "pending" entry automatically. Do not refactor into a pure reader.
|
|
93
|
+
def gate_states(task)
|
|
94
|
+
migrate_legacy_gate_state!(task)
|
|
95
|
+
task["gate_states"] ||= {}
|
|
96
|
+
gates.each do |gate|
|
|
97
|
+
key = gate_key(gate["agent"])
|
|
98
|
+
task["gate_states"][key] ||= new_gate_state(gate)
|
|
99
|
+
end
|
|
100
|
+
task["gate_states"]
|
|
72
101
|
end
|
|
73
102
|
|
|
74
103
|
# Sync gate approvals from GitHub PR reviews (self-healing).
|
|
@@ -110,22 +139,15 @@ module Brainiac
|
|
|
110
139
|
next unless review_state
|
|
111
140
|
|
|
112
141
|
if review_state == "approved"
|
|
113
|
-
|
|
114
|
-
|
|
142
|
+
state = gate_state(task, agent, role)
|
|
143
|
+
was_changes_requested = state["status"] == "changes_requested"
|
|
144
|
+
unless state["status"] == "approved"
|
|
115
145
|
record_approval(task, agent: agent, role: role)
|
|
116
146
|
changes[:approvals_added] << agent
|
|
117
147
|
end
|
|
118
|
-
|
|
119
|
-
if task["changes_requested_by"]&.include?(agent)
|
|
120
|
-
task["changes_requested_by"].delete(agent)
|
|
121
|
-
changes[:changes_cleared] << agent
|
|
122
|
-
end
|
|
148
|
+
changes[:changes_cleared] << agent if was_changes_requested
|
|
123
149
|
elsif review_state == "changes_requested"
|
|
124
|
-
|
|
125
|
-
task["gate_approvals"]&.reject! { |a| a["agent"].downcase == agent.downcase }
|
|
126
|
-
# Track changes_requested
|
|
127
|
-
task["changes_requested_by"] ||= []
|
|
128
|
-
task["changes_requested_by"] << agent unless task["changes_requested_by"].include?(agent)
|
|
150
|
+
record_changes_requested(task, agent: agent, role: role)
|
|
129
151
|
end
|
|
130
152
|
end
|
|
131
153
|
|
|
@@ -140,22 +162,31 @@ module Brainiac
|
|
|
140
162
|
# @param agent [String] Agent that approved
|
|
141
163
|
# @param role [String] Gate role
|
|
142
164
|
def record_approval(task, agent:, role:)
|
|
143
|
-
task
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
165
|
+
state = gate_state(task, agent, role)
|
|
166
|
+
changed = state["status"] != "approved"
|
|
167
|
+
state["status"] = "approved"
|
|
168
|
+
state["responded_at"] = Time.now.iso8601 if changed || state["responded_at"].nil?
|
|
169
|
+
state
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def record_changes_requested(task, agent:, role:)
|
|
173
|
+
state = gate_state(task, agent, role)
|
|
174
|
+
changed = state["status"] != "changes_requested"
|
|
175
|
+
state["status"] = "changes_requested"
|
|
176
|
+
state["responded_at"] = Time.now.iso8601 if changed || state["responded_at"].nil?
|
|
177
|
+
state
|
|
152
178
|
end
|
|
153
179
|
|
|
154
180
|
# Reset gate approvals (when changes are requested and code is updated).
|
|
155
181
|
#
|
|
156
182
|
# @param task [Hash] Task state (mutated in place)
|
|
157
183
|
def reset_approvals(task)
|
|
158
|
-
task
|
|
184
|
+
gate_states(task).each_value do |state|
|
|
185
|
+
state["status"] = "pending"
|
|
186
|
+
state["dispatched_at"] = nil
|
|
187
|
+
state["responded_at"] = nil
|
|
188
|
+
state["dispatch_count"] = 0
|
|
189
|
+
end
|
|
159
190
|
end
|
|
160
191
|
|
|
161
192
|
# Dispatch all gate agents to review a PR in parallel.
|
|
@@ -166,23 +197,17 @@ module Brainiac
|
|
|
166
197
|
# @param pr_number [Integer, String] PR number
|
|
167
198
|
# @param repo_name [String] e.g. "stowzilla/brainiac-basecamp"
|
|
168
199
|
# @param repo_path [String] Local repo path
|
|
169
|
-
# @param is_rereview [Boolean] True if this is a re-review after changes
|
|
170
200
|
# @return [Array<String>] Agent names dispatched
|
|
171
|
-
def dispatch_gates(epic:, task:, pr_number:, repo_name:, repo_path
|
|
201
|
+
def dispatch_gates(epic:, task:, pr_number:, repo_name:, repo_path:)
|
|
172
202
|
dispatched = []
|
|
173
203
|
|
|
174
|
-
|
|
175
|
-
|
|
204
|
+
gate_states(task).each_value do |state|
|
|
205
|
+
next unless state["status"] == "pending"
|
|
176
206
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
diff_from_sha = is_rereview && last_reviewed_sha ? last_reviewed_sha : nil
|
|
207
|
+
agent_name = state["agent"]
|
|
208
|
+
role = state["role"] || "review"
|
|
180
209
|
|
|
181
|
-
|
|
182
|
-
agent_name = gate["agent"]
|
|
183
|
-
role = gate["role"] || "review"
|
|
184
|
-
|
|
185
|
-
LOG.info "[Basecamp:ReviewGate] Dispatching #{agent_name} (#{role}) to review PR ##{pr_number}#{" (incremental from #{diff_from_sha[0..7]})" if diff_from_sha}" if defined?(LOG)
|
|
210
|
+
LOG.info "[Basecamp:ReviewGate] Dispatching #{agent_name} (#{role}) to review PR ##{pr_number}" if defined?(LOG)
|
|
186
211
|
|
|
187
212
|
# Dispatch the gate agent via brainiac-github's PR review mechanism.
|
|
188
213
|
# The agent gets the PR diff and reviews it using their bot identity.
|
|
@@ -194,102 +219,32 @@ module Brainiac
|
|
|
194
219
|
repo_name: repo_name,
|
|
195
220
|
repo_path: repo_path,
|
|
196
221
|
card_number: task["fizzy_card"],
|
|
197
|
-
epic: epic
|
|
198
|
-
diff_from_sha: diff_from_sha
|
|
222
|
+
epic: epic
|
|
199
223
|
)
|
|
200
224
|
rescue StandardError => e
|
|
201
225
|
LOG.error "[Basecamp:ReviewGate] Failed to dispatch #{agent_name}: #{e.message}" if defined?(LOG)
|
|
202
226
|
end
|
|
203
227
|
|
|
204
228
|
dispatched << agent_name
|
|
229
|
+
state["status"] = "dispatched"
|
|
230
|
+
state["dispatched_at"] = Time.now.iso8601
|
|
231
|
+
state["dispatch_count"] += 1
|
|
205
232
|
end
|
|
206
233
|
|
|
207
234
|
# Update task state
|
|
208
|
-
task
|
|
209
|
-
task["gates_dispatched_at"] = Time.now.iso8601
|
|
210
|
-
task["gate_approvals"] ||= []
|
|
211
|
-
|
|
212
|
-
# Track the SHA we're reviewing for incremental diff on next round
|
|
213
|
-
task["last_reviewed_sha"] = current_sha if current_sha
|
|
235
|
+
TaskState.transition!(task, :submit_for_review, triggered_by: "review_gate_dispatch")
|
|
214
236
|
|
|
215
237
|
dispatched
|
|
216
238
|
end
|
|
217
239
|
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
#
|
|
224
|
-
# IMPORTANT: Does NOT overwrite gates_dispatched_at — that timestamp represents
|
|
225
|
-
# the original dispatch and is used for stale detection. Overwriting it would
|
|
226
|
-
# reset the timeout window on every partial re-dispatch, causing infinite loops.
|
|
227
|
-
#
|
|
228
|
-
# @param epic [Hash] Epic state
|
|
229
|
-
# @param task [Hash] Task state
|
|
230
|
-
# @param pr_number [Integer, String] PR number
|
|
231
|
-
# @param repo_name [String] e.g. "stowzilla/brainiac-basecamp"
|
|
232
|
-
# @param repo_path [String] Local repo path
|
|
233
|
-
# @param is_rereview [Boolean] True if this is a re-review after implementation fixes
|
|
234
|
-
# @return [Array<String>] Agent names dispatched
|
|
235
|
-
def dispatch_missing_gates(epic:, task:, pr_number:, repo_name:, repo_path:, is_rereview: false)
|
|
236
|
-
responded_agents = Set.new
|
|
237
|
-
(task["gate_approvals"] || []).each { |a| responded_agents << a["agent"].downcase }
|
|
238
|
-
(task["changes_requested_by"] || []).each { |a| responded_agents << a.downcase }
|
|
239
|
-
|
|
240
|
-
missing_gates = gates.reject { |g| responded_agents.include?(g["agent"].downcase) }
|
|
241
|
-
return [] if missing_gates.empty?
|
|
242
|
-
|
|
243
|
-
# Track per-gate re-dispatch counts to prevent infinite loops
|
|
244
|
-
task["gate_redispatch_counts"] ||= {}
|
|
245
|
-
|
|
246
|
-
# For re-reviews, determine incremental diff range
|
|
247
|
-
diff_from_sha = is_rereview && task["last_reviewed_sha"] ? task["last_reviewed_sha"] : nil
|
|
248
|
-
current_sha = get_pr_head_sha(pr_number: pr_number, repo_path: repo_path)
|
|
249
|
-
|
|
250
|
-
dispatched = []
|
|
251
|
-
|
|
252
|
-
missing_gates.each do |gate|
|
|
253
|
-
agent_name = gate["agent"]
|
|
254
|
-
role = gate["role"] || "review"
|
|
255
|
-
|
|
256
|
-
# Check retry count — stop if we've exceeded the max
|
|
257
|
-
retries = task["gate_redispatch_counts"][agent_name.downcase] || 0
|
|
258
|
-
if retries >= MAX_GATE_REDISPATCH_RETRIES
|
|
259
|
-
LOG.warn "[Basecamp:ReviewGate] Gate #{agent_name} exceeded max re-dispatch retries (#{retries}/#{MAX_GATE_REDISPATCH_RETRIES}) — skipping" if defined?(LOG)
|
|
260
|
-
next
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
LOG.info "[Basecamp:ReviewGate] Re-dispatching missing gate #{agent_name} (#{role}) to review PR ##{pr_number} (retry #{retries + 1}/#{MAX_GATE_REDISPATCH_RETRIES})#{' (incremental)' if diff_from_sha}" if defined?(LOG)
|
|
264
|
-
|
|
265
|
-
task["gate_redispatch_counts"][agent_name.downcase] = retries + 1
|
|
266
|
-
|
|
267
|
-
Thread.new do
|
|
268
|
-
dispatch_agent_for_review(
|
|
269
|
-
agent_name: agent_name,
|
|
270
|
-
role: role,
|
|
271
|
-
pr_number: pr_number,
|
|
272
|
-
repo_name: repo_name,
|
|
273
|
-
repo_path: repo_path,
|
|
274
|
-
card_number: task["fizzy_card"],
|
|
275
|
-
epic: epic,
|
|
276
|
-
diff_from_sha: diff_from_sha
|
|
277
|
-
)
|
|
278
|
-
rescue StandardError => e
|
|
279
|
-
LOG.error "[Basecamp:ReviewGate] Failed to dispatch #{agent_name}: #{e.message}" if defined?(LOG)
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
dispatched << agent_name
|
|
240
|
+
# Re-dispatch only stale records. A gate that has used its full retry
|
|
241
|
+
# budget becomes explicitly timed_out instead of silently disappearing.
|
|
242
|
+
def redispatch_stale_gates(epic:, task:, pr_number:, repo_name:, repo_path:)
|
|
243
|
+
stale_gate_states(task).each do |state|
|
|
244
|
+
state["status"] = state["dispatch_count"] > MAX_GATE_REDISPATCH_RETRIES ? "timed_out" : "pending"
|
|
283
245
|
end
|
|
284
246
|
|
|
285
|
-
|
|
286
|
-
# but do NOT overwrite gates_dispatched_at — that's the original timestamp
|
|
287
|
-
task["last_redispatch_at"] = Time.now.iso8601
|
|
288
|
-
|
|
289
|
-
# Update last_reviewed_sha for next round
|
|
290
|
-
task["last_reviewed_sha"] = current_sha if current_sha
|
|
291
|
-
|
|
292
|
-
dispatched
|
|
247
|
+
dispatch_gates(epic: epic, task: task, pr_number: pr_number, repo_name: repo_name, repo_path: repo_path)
|
|
293
248
|
end
|
|
294
249
|
|
|
295
250
|
# Build the summary comment for Fizzy after all gates pass and merge completes.
|
|
@@ -298,7 +253,7 @@ module Brainiac
|
|
|
298
253
|
# @param pr_url [String] PR URL
|
|
299
254
|
# @return [String] HTML comment for Fizzy
|
|
300
255
|
def build_gate_summary_comment(task, pr_url:)
|
|
301
|
-
approvals = task["
|
|
256
|
+
approvals = gate_states(task).values.select { |state| state["status"] == "approved" }
|
|
302
257
|
lines = []
|
|
303
258
|
lines << "<p>✅ <strong>All review gates passed</strong> — merged into epic branch.</p>"
|
|
304
259
|
lines << "<p><a href=\"#{pr_url}\">PR Link</a></p>"
|
|
@@ -321,33 +276,72 @@ module Brainiac
|
|
|
321
276
|
|
|
322
277
|
private
|
|
323
278
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
# @param repo_path [String] Local repo path
|
|
328
|
-
# @return [String, nil] Commit SHA or nil if failed
|
|
329
|
-
def get_pr_head_sha(pr_number:, repo_path:)
|
|
330
|
-
stdout, _, status = Open3.capture3(
|
|
331
|
-
"gh", "pr", "view", pr_number.to_s,
|
|
332
|
-
"--json", "headRefOid",
|
|
333
|
-
"--jq", ".headRefOid",
|
|
334
|
-
chdir: repo_path
|
|
335
|
-
)
|
|
336
|
-
return nil unless status.success?
|
|
279
|
+
def gate_key(agent)
|
|
280
|
+
agent.to_s.downcase
|
|
281
|
+
end
|
|
337
282
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
283
|
+
def new_gate_state(gate)
|
|
284
|
+
{
|
|
285
|
+
"agent" => gate["agent"],
|
|
286
|
+
"role" => gate["role"] || "review",
|
|
287
|
+
"status" => "pending",
|
|
288
|
+
"dispatched_at" => nil,
|
|
289
|
+
"responded_at" => nil,
|
|
290
|
+
"dispatch_count" => 0
|
|
291
|
+
}
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def gate_state(task, agent, role = nil)
|
|
295
|
+
gate_states(task)[gate_key(agent)] ||= new_gate_state("agent" => agent, "role" => role)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def stale_gate_states(task)
|
|
299
|
+
gate_states(task).values.select do |state|
|
|
300
|
+
next false unless state["status"] == "dispatched" && state["dispatched_at"]
|
|
301
|
+
next false if SessionRegistry.alive?("gate-#{state['agent'].to_s.downcase}-#{task['fizzy_card']}")
|
|
302
|
+
|
|
303
|
+
Time.now - Time.parse(state["dispatched_at"]) > STALE_DISPATCH_TIMEOUT
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def migrate_legacy_gate_state!(task)
|
|
308
|
+
return if task["gate_states"]
|
|
309
|
+
|
|
310
|
+
task["gate_states"] = {}
|
|
311
|
+
approvals = task["gate_approvals"] || []
|
|
312
|
+
changes_requested = (task["changes_requested_by"] || []).map(&:downcase)
|
|
313
|
+
dispatched_at = task["gates_dispatched_at"]
|
|
314
|
+
redispatch_counts = task["gate_redispatch_counts"] || {}
|
|
315
|
+
|
|
316
|
+
gates.each do |gate|
|
|
317
|
+
key = gate_key(gate["agent"])
|
|
318
|
+
approval = approvals.find { |entry| gate_key(entry["agent"]) == key }
|
|
319
|
+
status = if approval
|
|
320
|
+
"approved"
|
|
321
|
+
elsif changes_requested.include?(key)
|
|
322
|
+
"changes_requested"
|
|
323
|
+
elsif dispatched_at
|
|
324
|
+
"dispatched"
|
|
325
|
+
else
|
|
326
|
+
"pending"
|
|
327
|
+
end
|
|
328
|
+
task["gate_states"][key] = new_gate_state(gate).merge(
|
|
329
|
+
"status" => status,
|
|
330
|
+
"dispatched_at" => dispatched_at,
|
|
331
|
+
"responded_at" => approval&.fetch("approved_at", nil),
|
|
332
|
+
"dispatch_count" => redispatch_counts[key] || (dispatched_at ? 1 : 0)
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
task.delete("gate_approvals")
|
|
337
|
+
task.delete("changes_requested_by")
|
|
338
|
+
task.delete("gates_dispatched_at")
|
|
339
|
+
task.delete("gate_redispatch_counts")
|
|
343
340
|
end
|
|
344
341
|
|
|
345
342
|
# Dispatch a single gate agent to review a PR.
|
|
346
343
|
# This creates a review prompt and runs the agent in the repo directory.
|
|
347
|
-
|
|
348
|
-
# @param diff_from_sha [String, nil] If present, this is a re-review and agent should
|
|
349
|
-
# focus on changes since this SHA (incremental review)
|
|
350
|
-
def dispatch_agent_for_review(agent_name:, role:, pr_number:, repo_name:, repo_path:, card_number:, epic:, diff_from_sha: nil)
|
|
344
|
+
def dispatch_agent_for_review(agent_name:, role:, pr_number:, repo_name:, repo_path:, card_number:, epic:)
|
|
351
345
|
# Build a review-specific prompt for the gate agent
|
|
352
346
|
prompt = build_gate_review_prompt(
|
|
353
347
|
agent_name: agent_name,
|
|
@@ -355,8 +349,7 @@ module Brainiac
|
|
|
355
349
|
pr_number: pr_number,
|
|
356
350
|
repo_name: repo_name,
|
|
357
351
|
card_number: card_number,
|
|
358
|
-
epic_title: epic["title"]
|
|
359
|
-
diff_from_sha: diff_from_sha
|
|
352
|
+
epic_title: epic["title"]
|
|
360
353
|
)
|
|
361
354
|
|
|
362
355
|
# Resolve the agent's GitHub token for their bot identity
|
|
@@ -396,89 +389,43 @@ module Brainiac
|
|
|
396
389
|
end
|
|
397
390
|
end
|
|
398
391
|
|
|
399
|
-
# Register session for
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
392
|
+
# Register session in SessionRegistry for liveness tracking
|
|
393
|
+
return unless pid
|
|
394
|
+
|
|
395
|
+
SessionRegistry.register_session(
|
|
396
|
+
card_key, pid,
|
|
397
|
+
log_file: log_file,
|
|
398
|
+
agent_name: agent_name,
|
|
399
|
+
epic_id: epic["id"],
|
|
400
|
+
card_number: card_number
|
|
401
|
+
)
|
|
402
|
+
LOG.info "[Basecamp:ReviewGate] Spawned #{agent_name} (pid #{pid}) for gate review on card ##{card_number}" if defined?(LOG)
|
|
405
403
|
end
|
|
406
404
|
|
|
407
405
|
# Build the prompt for a gate review agent.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
# Incremental re-review prompt — focus only on new changes
|
|
413
|
-
<<~PROMPT
|
|
414
|
-
You are RE-REVIEWING PR ##{pr_number} on #{repo_name} as part of epic: "#{epic_title}".
|
|
415
|
-
Your role: **#{role}**
|
|
416
|
-
|
|
417
|
-
This is an INCREMENTAL REVIEW — you already reviewed this PR and requested changes.
|
|
418
|
-
The implementation agent has pushed fixes. Focus on what changed.
|
|
419
|
-
|
|
420
|
-
**Step 1: Read the context to understand WHY changes were made:**
|
|
421
|
-
```
|
|
422
|
-
gh pr view #{pr_number}
|
|
423
|
-
gh pr view #{pr_number} --comments
|
|
424
|
-
git log #{diff_from_sha}..HEAD --oneline
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
- `gh pr view` shows the PR description (implementation rationale)
|
|
428
|
-
- `--comments` shows discussion and review responses
|
|
429
|
-
- `git log` shows commit messages explaining each fix
|
|
430
|
-
|
|
431
|
-
Read these BEFORE looking at the code changes.
|
|
432
|
-
|
|
433
|
-
**Step 2: View the NEW changes since your last review:**
|
|
434
|
-
```
|
|
435
|
-
git fetch origin && git diff #{diff_from_sha}..HEAD
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
Or compare in GitHub: `#{repo_name}/compare/#{diff_from_sha[0..7]}...HEAD`
|
|
439
|
-
|
|
440
|
-
If you need the full context, you can still use `gh pr diff #{pr_number}`, but prioritize
|
|
441
|
-
reviewing the incremental changes first.
|
|
442
|
-
|
|
443
|
-
Based on your role (#{role}):
|
|
444
|
-
#{role_instructions(role)}
|
|
445
|
-
|
|
446
|
-
After your review:
|
|
447
|
-
- If the fixes address your concerns: `gh pr review #{pr_number} --approve --body "your summary"`
|
|
448
|
-
- If more changes are needed: `gh pr review #{pr_number} --request-changes --body "what still needs fixing"`
|
|
449
|
-
|
|
450
|
-
Be thorough but pragmatic. This is Fizzy card ##{card_number}.
|
|
451
|
-
|
|
452
|
-
IMPORTANT RESTRICTIONS:
|
|
453
|
-
- Do NOT open new PRs or modify code — you are a reviewer only
|
|
454
|
-
- Do NOT comment on the Fizzy card — your review goes on GitHub only
|
|
455
|
-
- Do NOT use the fizzy CLI at all
|
|
456
|
-
PROMPT
|
|
457
|
-
else
|
|
458
|
-
# Initial full review prompt
|
|
459
|
-
<<~PROMPT
|
|
460
|
-
You are reviewing PR ##{pr_number} on #{repo_name} as part of epic: "#{epic_title}".
|
|
461
|
-
Your role: **#{role}**
|
|
406
|
+
def build_gate_review_prompt(agent_name:, role:, pr_number:, repo_name:, card_number:, epic_title:)
|
|
407
|
+
<<~PROMPT
|
|
408
|
+
You are reviewing PR ##{pr_number} on #{repo_name} as part of epic: "#{epic_title}".
|
|
409
|
+
Your role: **#{role}**
|
|
462
410
|
|
|
463
|
-
|
|
411
|
+
This is a review gate — the epic cannot proceed until you approve.
|
|
464
412
|
|
|
465
|
-
|
|
413
|
+
Review the PR changes with `gh pr diff #{pr_number}` and `gh pr view #{pr_number}`.
|
|
466
414
|
|
|
467
|
-
|
|
468
|
-
|
|
415
|
+
Based on your role (#{role}):
|
|
416
|
+
#{role_instructions(role)}
|
|
469
417
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
418
|
+
After your review:
|
|
419
|
+
- If the code meets your standards: `gh pr review #{pr_number} --approve --body "your summary"`
|
|
420
|
+
- If changes are needed: `gh pr review #{pr_number} --request-changes --body "what needs fixing"`
|
|
473
421
|
|
|
474
|
-
|
|
422
|
+
Be thorough but pragmatic. This is Fizzy card ##{card_number}.
|
|
475
423
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
end
|
|
424
|
+
IMPORTANT RESTRICTIONS:
|
|
425
|
+
- Do NOT open new PRs or modify code — you are a reviewer only
|
|
426
|
+
- Do NOT comment on the Fizzy card — your review goes on GitHub only
|
|
427
|
+
- Do NOT use the fizzy CLI at all
|
|
428
|
+
PROMPT
|
|
482
429
|
end
|
|
483
430
|
|
|
484
431
|
# Role-specific review instructions.
|