brainiac-basecamp 0.0.7 → 0.0.11
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4d21875fecdd7aa33718a8db5fa446800e8a85924490b6716a455b211d79483
|
|
4
|
+
data.tar.gz: 9601289488c260e891a6df6da2b63be3e9965c9c8235d11c4366622d27a24d0b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f24339950cce469195888d59fbc7ddecb0b75f9a7f8279df58cc2bb681637916525341ce8b9fc86c2f38bb9e8b1718a27c9f164d14afa4acff9376f4394da3eb
|
|
7
|
+
data.tar.gz: 39d9128c65180e56fe30e59d6b384dbc9ce2b7648f8586498b88e7e44b4c516fcea6d06b279bc1956373f84f1a8602aa1fb697699a40b98e699b2c6ef2f97c59
|
|
@@ -214,16 +214,12 @@ module Brainiac
|
|
|
214
214
|
# All gates have reviewed — dispatch implementation agent to address ALL feedback
|
|
215
215
|
LOG.info "[Basecamp:Hooks] All gates responded for card ##{card_number} — dispatching fixes" if defined?(LOG)
|
|
216
216
|
task["status"] = "in_flight"
|
|
217
|
+
task["dispatched_at"] = Time.now.iso8601
|
|
217
218
|
task.delete("changes_debounce_started")
|
|
218
219
|
save_epic_state(epic)
|
|
219
220
|
|
|
220
|
-
#
|
|
221
|
-
|
|
222
|
-
fizzy_user_id = Orchestrator.send(:resolve_fizzy_user_id, impl_agent)
|
|
223
|
-
if fizzy_user_id
|
|
224
|
-
LOG.info "[Basecamp:Hooks] Re-assigning card ##{card_number} to #{impl_agent} for fixes" if defined?(LOG)
|
|
225
|
-
safe_assign_card(card_number, fizzy_user_id)
|
|
226
|
-
end
|
|
221
|
+
# Spawn agent directly — safe_assign_card won't work if already assigned
|
|
222
|
+
dispatch_impl_directly(epic, task)
|
|
227
223
|
else
|
|
228
224
|
# Wait for remaining gates to respond
|
|
229
225
|
responded = (task["gate_approvals"]&.size || 0) + (task["changes_requested_by"]&.size || 0)
|
|
@@ -246,11 +242,11 @@ module Brainiac
|
|
|
246
242
|
if task_reloaded && task_reloaded["status"] == "in_review" && task_reloaded["changes_requested_by"]&.any?
|
|
247
243
|
LOG.info "[Basecamp:Hooks] Debounce timeout for card ##{card_number} — forcing dispatch" if defined?(LOG)
|
|
248
244
|
task_reloaded["status"] = "in_flight"
|
|
245
|
+
task_reloaded["dispatched_at"] = Time.now.iso8601
|
|
249
246
|
task_reloaded.delete("changes_debounce_started")
|
|
250
247
|
save_epic_state(epic_reloaded)
|
|
251
|
-
#
|
|
252
|
-
|
|
253
|
-
safe_assign_card(card_number, fizzy_user_id) if fizzy_user_id
|
|
248
|
+
# Spawn agent directly — safe_assign_card won't work if already assigned
|
|
249
|
+
dispatch_impl_directly(epic_reloaded, task_reloaded)
|
|
254
250
|
end
|
|
255
251
|
rescue StandardError => e
|
|
256
252
|
LOG.error "[Basecamp:Hooks] Debounce dispatch failed: #{e.message}" if defined?(LOG)
|
|
@@ -728,14 +724,123 @@ module Brainiac
|
|
|
728
724
|
end
|
|
729
725
|
end
|
|
730
726
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
727
|
+
# Dispatch the implementation agent directly (spawns via run_agent) to address
|
|
728
|
+
# changes_requested feedback. Used by resume and health-check when the agent is
|
|
729
|
+
# no longer running but the Fizzy card is still assigned (so safe_assign_card
|
|
730
|
+
# would bail out with "already assigned, skipping").
|
|
731
|
+
#
|
|
732
|
+
# @param epic [Hash] Epic state
|
|
733
|
+
# @param task [Hash] Task within the epic
|
|
734
|
+
# @return [void]
|
|
735
|
+
def dispatch_impl_directly(epic, task)
|
|
736
|
+
card_number = task["fizzy_card"]
|
|
737
|
+
agent_name = epic["agent"]
|
|
738
|
+
pr_number = task["pr_number"]
|
|
739
|
+
project_key = task["project"]
|
|
740
|
+
card_key = "changes-requested-#{card_number}"
|
|
741
|
+
|
|
742
|
+
# Guard: don't spawn a duplicate if a session is already running for this task
|
|
743
|
+
session_alive = if defined?(session_active?)
|
|
744
|
+
session_active?(card_key)
|
|
745
|
+
elsif Object.respond_to?(:session_active?, true)
|
|
746
|
+
Object.send(:session_active?, card_key)
|
|
747
|
+
else
|
|
748
|
+
false
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
if session_alive
|
|
752
|
+
LOG.info "[Basecamp:Hooks] Session already active for #{card_key} — skipping dispatch" if defined?(LOG)
|
|
753
|
+
return
|
|
754
|
+
end
|
|
734
755
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
756
|
+
LOG.info "[Basecamp:Hooks] Direct-dispatching #{agent_name} for changes_requested on card ##{card_number}" if defined?(LOG)
|
|
757
|
+
|
|
758
|
+
# Get project config
|
|
759
|
+
projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
|
|
760
|
+
projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
761
|
+
project_config = projects[project_key]
|
|
762
|
+
|
|
763
|
+
unless project_config
|
|
764
|
+
LOG.error "[Basecamp:Hooks] No project config for '#{project_key}' — cannot direct-dispatch" if defined?(LOG)
|
|
765
|
+
return
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
repo_path = project_config["repo_path"]
|
|
769
|
+
unless repo_path
|
|
770
|
+
LOG.error "[Basecamp:Hooks] No repo_path for project '#{project_key}' — cannot direct-dispatch" if defined?(LOG)
|
|
771
|
+
return
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
# Find the worktree for this card
|
|
775
|
+
work_items_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "work_items.json")
|
|
776
|
+
worktree_path = repo_path # Default to main repo
|
|
777
|
+
if File.exist?(work_items_file)
|
|
778
|
+
work_items = JSON.parse(File.read(work_items_file))
|
|
779
|
+
work_item = work_items.values.find { |wi| wi.dig("sources", "fizzy", "card_number") == card_number }
|
|
780
|
+
worktree_path = work_item["worktree"] if work_item&.dig("worktree")
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
# Build prompt
|
|
784
|
+
changers = (task["changes_requested_by"] || []).join(", ")
|
|
785
|
+
prompt = <<~PROMPT
|
|
786
|
+
## Changes Requested — Fizzy Card ##{card_number}
|
|
787
|
+
|
|
788
|
+
Review gates have requested changes on your PR#{pr_number ? " ##{pr_number}" : ""}.
|
|
789
|
+
Reviewers who requested changes: #{changers}
|
|
790
|
+
|
|
791
|
+
Your job:
|
|
792
|
+
1. Read the review feedback: #{pr_number ? "`gh pr view #{pr_number} --comments`" : "check the Fizzy card comments"}
|
|
793
|
+
2. Address all requested changes
|
|
794
|
+
3. Commit and push your fixes
|
|
795
|
+
|
|
796
|
+
After pushing, update the Fizzy card with a brief status comment noting what you fixed.
|
|
797
|
+
PROMPT
|
|
798
|
+
|
|
799
|
+
# Resolve GitHub App token so agent's `gh` commands run as their bot identity
|
|
800
|
+
github_repo = project_config&.dig("github_repo")
|
|
801
|
+
agent_env = github_repo ? ReviewGate.send(:resolve_agent_github_env, agent_name, github_repo) : {}
|
|
802
|
+
|
|
803
|
+
# Spawn the agent directly
|
|
804
|
+
pid = nil
|
|
805
|
+
log_file = nil
|
|
806
|
+
|
|
807
|
+
begin
|
|
808
|
+
pid, log_file = method(:run_agent).call(
|
|
809
|
+
prompt,
|
|
810
|
+
project_config: project_config,
|
|
811
|
+
chdir: worktree_path,
|
|
812
|
+
log_name: "changes-requested-#{card_number}",
|
|
813
|
+
agent_name: agent_name,
|
|
814
|
+
source: :basecamp,
|
|
815
|
+
card_number: card_number,
|
|
816
|
+
env: agent_env
|
|
817
|
+
)
|
|
818
|
+
rescue NameError
|
|
819
|
+
if Object.respond_to?(:run_agent, true)
|
|
820
|
+
pid, log_file = Object.send(:run_agent,
|
|
821
|
+
prompt,
|
|
822
|
+
project_config: project_config,
|
|
823
|
+
chdir: worktree_path,
|
|
824
|
+
log_name: "changes-requested-#{card_number}",
|
|
825
|
+
agent_name: agent_name,
|
|
826
|
+
source: :basecamp,
|
|
827
|
+
card_number: card_number,
|
|
828
|
+
env: agent_env)
|
|
829
|
+
else
|
|
830
|
+
LOG.warn "[Basecamp:Hooks] run_agent not available — direct impl dispatch skipped" if defined?(LOG)
|
|
831
|
+
return
|
|
832
|
+
end
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# Register session for waybar visibility
|
|
836
|
+
if pid
|
|
837
|
+
if defined?(register_session)
|
|
838
|
+
register_session(card_key, pid, log_file: log_file, agent_name: agent_name)
|
|
839
|
+
elsif Object.respond_to?(:register_session, true)
|
|
840
|
+
Object.send(:register_session, card_key, pid, log_file: log_file, agent_name: agent_name)
|
|
841
|
+
end
|
|
842
|
+
LOG.info "[Basecamp:Hooks] Spawned #{agent_name} (pid #{pid}) for changes_requested on card ##{card_number}" if defined?(LOG)
|
|
843
|
+
end
|
|
739
844
|
end
|
|
740
845
|
|
|
741
846
|
def auto_merge_and_advance(epic, card_number, ctx)
|
|
@@ -203,6 +203,74 @@ module Brainiac
|
|
|
203
203
|
dispatched
|
|
204
204
|
end
|
|
205
205
|
|
|
206
|
+
# Dispatch only gate agents that have NOT yet responded (no approval, no changes_requested).
|
|
207
|
+
# Used during resume/health-check when some gates responded but others didn't.
|
|
208
|
+
#
|
|
209
|
+
# Tracks per-gate re-dispatch count to prevent infinite loops when a gate agent
|
|
210
|
+
# keeps crashing. After MAX_GATE_REDISPATCH_RETRIES, stops retrying that gate.
|
|
211
|
+
#
|
|
212
|
+
# IMPORTANT: Does NOT overwrite gates_dispatched_at — that timestamp represents
|
|
213
|
+
# the original dispatch and is used for stale detection. Overwriting it would
|
|
214
|
+
# reset the timeout window on every partial re-dispatch, causing infinite loops.
|
|
215
|
+
#
|
|
216
|
+
# @param epic [Hash] Epic state
|
|
217
|
+
# @param task [Hash] Task state
|
|
218
|
+
# @param pr_number [Integer, String] PR number
|
|
219
|
+
# @param repo_name [String] e.g. "stowzilla/brainiac-basecamp"
|
|
220
|
+
# @param repo_path [String] Local repo path
|
|
221
|
+
# @return [Array<String>] Agent names dispatched
|
|
222
|
+
def dispatch_missing_gates(epic:, task:, pr_number:, repo_name:, repo_path:)
|
|
223
|
+
responded_agents = Set.new
|
|
224
|
+
(task["gate_approvals"] || []).each { |a| responded_agents << a["agent"].downcase }
|
|
225
|
+
(task["changes_requested_by"] || []).each { |a| responded_agents << a.downcase }
|
|
226
|
+
|
|
227
|
+
missing_gates = gates.reject { |g| responded_agents.include?(g["agent"].downcase) }
|
|
228
|
+
return [] if missing_gates.empty?
|
|
229
|
+
|
|
230
|
+
# Track per-gate re-dispatch counts to prevent infinite loops
|
|
231
|
+
task["gate_redispatch_counts"] ||= {}
|
|
232
|
+
|
|
233
|
+
dispatched = []
|
|
234
|
+
|
|
235
|
+
missing_gates.each do |gate|
|
|
236
|
+
agent_name = gate["agent"]
|
|
237
|
+
role = gate["role"] || "review"
|
|
238
|
+
|
|
239
|
+
# Check retry count — stop if we've exceeded the max
|
|
240
|
+
retries = task["gate_redispatch_counts"][agent_name.downcase] || 0
|
|
241
|
+
if retries >= MAX_GATE_REDISPATCH_RETRIES
|
|
242
|
+
LOG.warn "[Basecamp:ReviewGate] Gate #{agent_name} exceeded max re-dispatch retries (#{retries}/#{MAX_GATE_REDISPATCH_RETRIES}) — skipping" if defined?(LOG)
|
|
243
|
+
next
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
LOG.info "[Basecamp:ReviewGate] Re-dispatching missing gate #{agent_name} (#{role}) to review PR ##{pr_number} (retry #{retries + 1}/#{MAX_GATE_REDISPATCH_RETRIES})" if defined?(LOG)
|
|
247
|
+
|
|
248
|
+
task["gate_redispatch_counts"][agent_name.downcase] = retries + 1
|
|
249
|
+
|
|
250
|
+
Thread.new do
|
|
251
|
+
dispatch_agent_for_review(
|
|
252
|
+
agent_name: agent_name,
|
|
253
|
+
role: role,
|
|
254
|
+
pr_number: pr_number,
|
|
255
|
+
repo_name: repo_name,
|
|
256
|
+
repo_path: repo_path,
|
|
257
|
+
card_number: task["fizzy_card"],
|
|
258
|
+
epic: epic
|
|
259
|
+
)
|
|
260
|
+
rescue StandardError => e
|
|
261
|
+
LOG.error "[Basecamp:ReviewGate] Failed to dispatch #{agent_name}: #{e.message}" if defined?(LOG)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
dispatched << agent_name
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Record when this partial re-dispatch happened (for diagnostics)
|
|
268
|
+
# but do NOT overwrite gates_dispatched_at — that's the original timestamp
|
|
269
|
+
task["last_redispatch_at"] = Time.now.iso8601
|
|
270
|
+
|
|
271
|
+
dispatched
|
|
272
|
+
end
|
|
273
|
+
|
|
206
274
|
# Build the summary comment for Fizzy after all gates pass and merge completes.
|
|
207
275
|
#
|
|
208
276
|
# @param task [Hash] Task state
|
|
@@ -16,6 +16,15 @@ require_relative "basecamp/cli"
|
|
|
16
16
|
module Brainiac
|
|
17
17
|
module Plugins
|
|
18
18
|
module Basecamp
|
|
19
|
+
# Maximum seconds to wait for a dispatched agent/gate to respond before
|
|
20
|
+
# considering it stale and re-dispatching. Used across resume, health-check,
|
|
21
|
+
# and dispatch_missing_gates.
|
|
22
|
+
STALE_DISPATCH_TIMEOUT = 300 # 5 minutes
|
|
23
|
+
|
|
24
|
+
# Maximum number of times a gate will be re-dispatched for the same review
|
|
25
|
+
# cycle before giving up (prevents infinite re-dispatch loops).
|
|
26
|
+
MAX_GATE_REDISPATCH_RETRIES = 3
|
|
27
|
+
|
|
19
28
|
class << self
|
|
20
29
|
# Called by Brainiac plugin system during server startup.
|
|
21
30
|
#
|
|
@@ -107,22 +116,34 @@ module Brainiac
|
|
|
107
116
|
end
|
|
108
117
|
|
|
109
118
|
when "in_flight"
|
|
110
|
-
# Check if impl agent is
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
# Check if impl agent session is likely dead after a restart.
|
|
120
|
+
# Instead of just checking Fizzy assignment (stale artifact), use
|
|
121
|
+
# dispatched_at as a liveness signal — if dispatched too long ago
|
|
122
|
+
# without completion, assume the session died and re-dispatch.
|
|
123
|
+
dispatched_at = task["dispatched_at"]
|
|
124
|
+
if dispatched_at
|
|
125
|
+
elapsed = Time.now - Time.parse(dispatched_at)
|
|
126
|
+
# If task has been in_flight for longer than the stale timeout
|
|
127
|
+
# AND no agent is actively running (we can't check PID, so use elapsed time)
|
|
128
|
+
if elapsed > STALE_DISPATCH_TIMEOUT
|
|
129
|
+
impl_agent = epic["agent"]
|
|
130
|
+
LOG.info "[Basecamp:HealthCheck] Task ##{card_number} in_flight for #{elapsed.round}s — re-dispatching #{impl_agent}" if defined?(LOG)
|
|
131
|
+
|
|
132
|
+
# Spawn the agent directly — safe_assign_card won't work if already assigned
|
|
133
|
+
task["dispatched_at"] = Time.now.iso8601
|
|
134
|
+
Hooks.send(:dispatch_impl_directly, epic, task)
|
|
135
|
+
healed_any = true
|
|
125
136
|
end
|
|
137
|
+
elsif task["changes_requested_by"]&.any?
|
|
138
|
+
# No dispatched_at recorded but changes were requested — legacy state.
|
|
139
|
+
# Always re-dispatch in this case.
|
|
140
|
+
impl_agent = epic["agent"]
|
|
141
|
+
LOG.info "[Basecamp:HealthCheck] Task ##{card_number} in_flight with changes_requested but no dispatched_at — re-dispatching #{impl_agent}" if defined?(LOG)
|
|
142
|
+
|
|
143
|
+
# Spawn the agent directly — safe_assign_card won't work if already assigned
|
|
144
|
+
task["dispatched_at"] = Time.now.iso8601
|
|
145
|
+
Hooks.send(:dispatch_impl_directly, epic, task)
|
|
146
|
+
healed_any = true
|
|
126
147
|
end
|
|
127
148
|
|
|
128
149
|
when "in_review"
|
|
@@ -168,7 +189,7 @@ module Brainiac
|
|
|
168
189
|
elsif (task["gate_approvals"] || []).empty? && !task["changes_requested_by"]&.any?
|
|
169
190
|
# No gate responses at all — gates may never have been dispatched or agents crashed
|
|
170
191
|
gates_dispatched_at = task["gates_dispatched_at"]
|
|
171
|
-
should_redispatch = gates_dispatched_at.nil? || (Time.now - Time.parse(gates_dispatched_at)) >
|
|
192
|
+
should_redispatch = gates_dispatched_at.nil? || (Time.now - Time.parse(gates_dispatched_at)) > STALE_DISPATCH_TIMEOUT
|
|
172
193
|
|
|
173
194
|
if should_redispatch && github_repo
|
|
174
195
|
LOG.info "[Basecamp:HealthCheck] No gate responses for ##{card_number} — re-dispatching review gates" if defined?(LOG)
|
|
@@ -182,6 +203,25 @@ module Brainiac
|
|
|
182
203
|
Hooks.send(:save_epic_state, epic)
|
|
183
204
|
healed_any = true
|
|
184
205
|
end
|
|
206
|
+
else
|
|
207
|
+
# Partial responses — some gates responded but others haven't
|
|
208
|
+
gates_dispatched_at = task["gates_dispatched_at"]
|
|
209
|
+
if gates_dispatched_at && (Time.now - Time.parse(gates_dispatched_at)) > STALE_DISPATCH_TIMEOUT
|
|
210
|
+
responded_count = (task["gate_approvals"]&.size || 0) + (task["changes_requested_by"]&.size || 0)
|
|
211
|
+
total_gates = ReviewGate.gates.size
|
|
212
|
+
if responded_count.positive? && responded_count < total_gates && github_repo
|
|
213
|
+
LOG.info "[Basecamp:HealthCheck] Partial gate response for ##{card_number} (#{responded_count}/#{total_gates}) — re-dispatching missing gates" if defined?(LOG)
|
|
214
|
+
ReviewGate.dispatch_missing_gates(
|
|
215
|
+
epic: epic,
|
|
216
|
+
task: task,
|
|
217
|
+
pr_number: pr_number,
|
|
218
|
+
repo_name: github_repo,
|
|
219
|
+
repo_path: repo_path
|
|
220
|
+
)
|
|
221
|
+
Hooks.send(:save_epic_state, epic)
|
|
222
|
+
healed_any = true
|
|
223
|
+
end
|
|
224
|
+
end
|
|
185
225
|
end
|
|
186
226
|
end
|
|
187
227
|
end
|
|
@@ -278,61 +318,68 @@ module Brainiac
|
|
|
278
318
|
Hooks.send(:save_epic_state, epic)
|
|
279
319
|
Hooks.send(:dispatch_final_decision, epic, task, {})
|
|
280
320
|
elsif task["changes_requested_by"]&.any?
|
|
281
|
-
# Gates requested changes —
|
|
321
|
+
# Gates requested changes — after a restart, no agent session is running even if assigned.
|
|
322
|
+
# Always re-dispatch the impl agent to address the feedback.
|
|
282
323
|
impl_agent = epic["agent"]
|
|
283
|
-
LOG.info "[Basecamp] Resume: ##{card_number} has changes requested
|
|
324
|
+
LOG.info "[Basecamp] Resume: ##{card_number} has changes requested — re-dispatching #{impl_agent}" if defined?(LOG)
|
|
284
325
|
|
|
285
|
-
#
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
assignee_names = assignees.map { |a| a["name"]&.downcase }
|
|
326
|
+
# Transition to in_flight so the agent gets the right context
|
|
327
|
+
task["status"] = "in_flight"
|
|
328
|
+
task["dispatched_at"] = Time.now.iso8601
|
|
329
|
+
Hooks.send(:save_epic_state, epic)
|
|
290
330
|
|
|
291
|
-
if
|
|
292
|
-
|
|
293
|
-
else
|
|
294
|
-
# Re-assign to trigger dispatch
|
|
295
|
-
LOG.info "[Basecamp] Resume: ##{card_number} not assigned to #{impl_agent} — re-assigning" if defined?(LOG)
|
|
296
|
-
fizzy_user_id = Orchestrator.send(:resolve_fizzy_user_id, impl_agent)
|
|
297
|
-
if fizzy_user_id
|
|
298
|
-
Open3.capture3("fizzy", "card", "assign", card_number.to_s, "--user", fizzy_user_id)
|
|
299
|
-
end
|
|
300
|
-
end
|
|
331
|
+
# Spawn the agent directly — safe_assign_card won't work if already assigned
|
|
332
|
+
Hooks.send(:dispatch_impl_directly, epic, task)
|
|
301
333
|
else
|
|
302
334
|
# No changes requested yet — check if gates need (re-)dispatching
|
|
303
335
|
approvals = task["gate_approvals"]&.size || 0
|
|
304
336
|
gates_dispatched_at = task["gates_dispatched_at"]
|
|
305
337
|
|
|
338
|
+
# Calculate how many gates have responded (approved or requested changes)
|
|
339
|
+
responded_count = approvals + (task["changes_requested_by"]&.size || 0)
|
|
340
|
+
total_gates = ReviewGate.gates.size
|
|
341
|
+
|
|
306
342
|
# Re-dispatch gates if:
|
|
307
343
|
# 1. Gates were never dispatched (missed webhook), OR
|
|
308
|
-
# 2. Gates were dispatched but no responses received after
|
|
344
|
+
# 2. Gates were dispatched but no responses received after 5 minutes, OR
|
|
345
|
+
# 3. Some gates responded but others didn't after 5 minutes (partial response)
|
|
309
346
|
should_redispatch = if gates_dispatched_at.nil?
|
|
310
347
|
true
|
|
311
|
-
elsif approvals.zero?
|
|
312
|
-
# If dispatched more than 5 minutes ago with no responses, re-dispatch
|
|
313
|
-
elapsed = Time.now - Time.parse(gates_dispatched_at)
|
|
314
|
-
elapsed > 300
|
|
315
348
|
else
|
|
316
|
-
|
|
349
|
+
elapsed = Time.now - Time.parse(gates_dispatched_at)
|
|
350
|
+
elapsed > STALE_DISPATCH_TIMEOUT && responded_count < total_gates
|
|
317
351
|
end
|
|
318
352
|
|
|
319
353
|
if should_redispatch && ReviewGate.enabled?
|
|
320
354
|
github_repo = projects.dig(project_key, "github_repo")
|
|
321
355
|
if github_repo
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
356
|
+
if responded_count.zero?
|
|
357
|
+
# No responses at all — dispatch all gates
|
|
358
|
+
LOG.info "[Basecamp] Resume: ##{card_number} has 0/#{total_gates} gate responses — re-dispatching all review gates" if defined?(LOG)
|
|
359
|
+
ReviewGate.dispatch_gates(
|
|
360
|
+
epic: epic,
|
|
361
|
+
task: task,
|
|
362
|
+
pr_number: pr_number,
|
|
363
|
+
repo_name: github_repo,
|
|
364
|
+
repo_path: repo_path
|
|
365
|
+
)
|
|
366
|
+
else
|
|
367
|
+
# Partial responses — dispatch only the missing gates
|
|
368
|
+
LOG.info "[Basecamp] Resume: ##{card_number} has #{responded_count}/#{total_gates} gate responses — re-dispatching missing gates" if defined?(LOG)
|
|
369
|
+
ReviewGate.dispatch_missing_gates(
|
|
370
|
+
epic: epic,
|
|
371
|
+
task: task,
|
|
372
|
+
pr_number: pr_number,
|
|
373
|
+
repo_name: github_repo,
|
|
374
|
+
repo_path: repo_path
|
|
375
|
+
)
|
|
376
|
+
end
|
|
330
377
|
Hooks.send(:save_epic_state, epic)
|
|
331
378
|
else
|
|
332
379
|
LOG.warn "[Basecamp] Resume: ##{card_number} — cannot re-dispatch gates, no github_repo for #{project_key}" if defined?(LOG)
|
|
333
380
|
end
|
|
334
381
|
else
|
|
335
|
-
LOG.info "[Basecamp] Resume: ##{card_number} has #{
|
|
382
|
+
LOG.info "[Basecamp] Resume: ##{card_number} has #{responded_count}/#{total_gates} gate responses, waiting for more reviews" if defined?(LOG)
|
|
336
383
|
end
|
|
337
384
|
end
|
|
338
385
|
end
|