brainiac-basecamp 0.0.7 → 0.0.9
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: f3b4d7d9e99a700f2a533b48110071025bde9eb69b55633acffd4125f88c5540
|
|
4
|
+
data.tar.gz: 63635c64597b65ed6e0a69e90511010d6975c418249c7a181d9a0349bded7dcc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37d7d00ab786c8f99318d7098b8a700d8428d0c3db1da4ceaa1040aa924b627b0b886a928cb7f6f1661e35f578403ae4b8911b87a5c48c6bc67a9c0ed5f61c02
|
|
7
|
+
data.tar.gz: a798eb76376698539c16c54ecdfea9d597893e0f0630c035e9e473401e45b43c0357e4cb6ef8717df704c98543f2bace4754b5f2db6dc97677a46ee8782b2ff9
|
|
@@ -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,38 @@ 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
|
-
|
|
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
|
+
# Re-dispatch by re-assigning the card (triggers webhook)
|
|
133
|
+
task["dispatched_at"] = Time.now.iso8601
|
|
120
134
|
fizzy_user_id = Orchestrator.send(:resolve_fizzy_user_id, impl_agent)
|
|
121
135
|
if fizzy_user_id
|
|
122
|
-
|
|
136
|
+
Hooks.send(:safe_assign_card, card_number, fizzy_user_id)
|
|
123
137
|
healed_any = true
|
|
124
138
|
end
|
|
125
139
|
end
|
|
140
|
+
elsif task["changes_requested_by"]&.any?
|
|
141
|
+
# No dispatched_at recorded but changes were requested — legacy state.
|
|
142
|
+
# Always re-dispatch in this case.
|
|
143
|
+
impl_agent = epic["agent"]
|
|
144
|
+
LOG.info "[Basecamp:HealthCheck] Task ##{card_number} in_flight with changes_requested but no dispatched_at — re-dispatching #{impl_agent}" if defined?(LOG)
|
|
145
|
+
task["dispatched_at"] = Time.now.iso8601
|
|
146
|
+
fizzy_user_id = Orchestrator.send(:resolve_fizzy_user_id, impl_agent)
|
|
147
|
+
if fizzy_user_id
|
|
148
|
+
Hooks.send(:safe_assign_card, card_number, fizzy_user_id)
|
|
149
|
+
healed_any = true
|
|
150
|
+
end
|
|
126
151
|
end
|
|
127
152
|
|
|
128
153
|
when "in_review"
|
|
@@ -168,7 +193,7 @@ module Brainiac
|
|
|
168
193
|
elsif (task["gate_approvals"] || []).empty? && !task["changes_requested_by"]&.any?
|
|
169
194
|
# No gate responses at all — gates may never have been dispatched or agents crashed
|
|
170
195
|
gates_dispatched_at = task["gates_dispatched_at"]
|
|
171
|
-
should_redispatch = gates_dispatched_at.nil? || (Time.now - Time.parse(gates_dispatched_at)) >
|
|
196
|
+
should_redispatch = gates_dispatched_at.nil? || (Time.now - Time.parse(gates_dispatched_at)) > STALE_DISPATCH_TIMEOUT
|
|
172
197
|
|
|
173
198
|
if should_redispatch && github_repo
|
|
174
199
|
LOG.info "[Basecamp:HealthCheck] No gate responses for ##{card_number} — re-dispatching review gates" if defined?(LOG)
|
|
@@ -182,6 +207,25 @@ module Brainiac
|
|
|
182
207
|
Hooks.send(:save_epic_state, epic)
|
|
183
208
|
healed_any = true
|
|
184
209
|
end
|
|
210
|
+
else
|
|
211
|
+
# Partial responses — some gates responded but others haven't
|
|
212
|
+
gates_dispatched_at = task["gates_dispatched_at"]
|
|
213
|
+
if gates_dispatched_at && (Time.now - Time.parse(gates_dispatched_at)) > STALE_DISPATCH_TIMEOUT
|
|
214
|
+
responded_count = (task["gate_approvals"]&.size || 0) + (task["changes_requested_by"]&.size || 0)
|
|
215
|
+
total_gates = ReviewGate.gates.size
|
|
216
|
+
if responded_count.positive? && responded_count < total_gates && github_repo
|
|
217
|
+
LOG.info "[Basecamp:HealthCheck] Partial gate response for ##{card_number} (#{responded_count}/#{total_gates}) — re-dispatching missing gates" if defined?(LOG)
|
|
218
|
+
ReviewGate.dispatch_missing_gates(
|
|
219
|
+
epic: epic,
|
|
220
|
+
task: task,
|
|
221
|
+
pr_number: pr_number,
|
|
222
|
+
repo_name: github_repo,
|
|
223
|
+
repo_path: repo_path
|
|
224
|
+
)
|
|
225
|
+
Hooks.send(:save_epic_state, epic)
|
|
226
|
+
healed_any = true
|
|
227
|
+
end
|
|
228
|
+
end
|
|
185
229
|
end
|
|
186
230
|
end
|
|
187
231
|
end
|
|
@@ -278,61 +322,70 @@ module Brainiac
|
|
|
278
322
|
Hooks.send(:save_epic_state, epic)
|
|
279
323
|
Hooks.send(:dispatch_final_decision, epic, task, {})
|
|
280
324
|
elsif task["changes_requested_by"]&.any?
|
|
281
|
-
# Gates requested changes —
|
|
325
|
+
# Gates requested changes — after a restart, no agent session is running even if assigned.
|
|
326
|
+
# Always re-dispatch the impl agent to address the feedback.
|
|
282
327
|
impl_agent = epic["agent"]
|
|
283
|
-
LOG.info "[Basecamp] Resume: ##{card_number} has changes requested
|
|
328
|
+
LOG.info "[Basecamp] Resume: ##{card_number} has changes requested — re-dispatching #{impl_agent}" if defined?(LOG)
|
|
284
329
|
|
|
285
|
-
#
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
assignees = card_data.dig("data", "assignees") || []
|
|
289
|
-
assignee_names = assignees.map { |a| a["name"]&.downcase }
|
|
330
|
+
# Transition to in_flight so the agent gets the right context
|
|
331
|
+
task["status"] = "in_flight"
|
|
332
|
+
Hooks.send(:save_epic_state, epic)
|
|
290
333
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
334
|
+
# Re-assign card to trigger a fresh dispatch via the Fizzy webhook
|
|
335
|
+
fizzy_user_id = Orchestrator.send(:resolve_fizzy_user_id, impl_agent)
|
|
336
|
+
if fizzy_user_id
|
|
337
|
+
Hooks.send(:safe_assign_card, card_number, fizzy_user_id)
|
|
300
338
|
end
|
|
301
339
|
else
|
|
302
340
|
# No changes requested yet — check if gates need (re-)dispatching
|
|
303
341
|
approvals = task["gate_approvals"]&.size || 0
|
|
304
342
|
gates_dispatched_at = task["gates_dispatched_at"]
|
|
305
343
|
|
|
344
|
+
# Calculate how many gates have responded (approved or requested changes)
|
|
345
|
+
responded_count = approvals + (task["changes_requested_by"]&.size || 0)
|
|
346
|
+
total_gates = ReviewGate.gates.size
|
|
347
|
+
|
|
306
348
|
# Re-dispatch gates if:
|
|
307
349
|
# 1. Gates were never dispatched (missed webhook), OR
|
|
308
|
-
# 2. Gates were dispatched but no responses received after
|
|
350
|
+
# 2. Gates were dispatched but no responses received after 5 minutes, OR
|
|
351
|
+
# 3. Some gates responded but others didn't after 5 minutes (partial response)
|
|
309
352
|
should_redispatch = if gates_dispatched_at.nil?
|
|
310
353
|
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
354
|
else
|
|
316
|
-
|
|
355
|
+
elapsed = Time.now - Time.parse(gates_dispatched_at)
|
|
356
|
+
elapsed > STALE_DISPATCH_TIMEOUT && responded_count < total_gates
|
|
317
357
|
end
|
|
318
358
|
|
|
319
359
|
if should_redispatch && ReviewGate.enabled?
|
|
320
360
|
github_repo = projects.dig(project_key, "github_repo")
|
|
321
361
|
if github_repo
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
362
|
+
if responded_count.zero?
|
|
363
|
+
# No responses at all — dispatch all gates
|
|
364
|
+
LOG.info "[Basecamp] Resume: ##{card_number} has 0/#{total_gates} gate responses — re-dispatching all review gates" if defined?(LOG)
|
|
365
|
+
ReviewGate.dispatch_gates(
|
|
366
|
+
epic: epic,
|
|
367
|
+
task: task,
|
|
368
|
+
pr_number: pr_number,
|
|
369
|
+
repo_name: github_repo,
|
|
370
|
+
repo_path: repo_path
|
|
371
|
+
)
|
|
372
|
+
else
|
|
373
|
+
# Partial responses — dispatch only the missing gates
|
|
374
|
+
LOG.info "[Basecamp] Resume: ##{card_number} has #{responded_count}/#{total_gates} gate responses — re-dispatching missing gates" if defined?(LOG)
|
|
375
|
+
ReviewGate.dispatch_missing_gates(
|
|
376
|
+
epic: epic,
|
|
377
|
+
task: task,
|
|
378
|
+
pr_number: pr_number,
|
|
379
|
+
repo_name: github_repo,
|
|
380
|
+
repo_path: repo_path
|
|
381
|
+
)
|
|
382
|
+
end
|
|
330
383
|
Hooks.send(:save_epic_state, epic)
|
|
331
384
|
else
|
|
332
385
|
LOG.warn "[Basecamp] Resume: ##{card_number} — cannot re-dispatch gates, no github_repo for #{project_key}" if defined?(LOG)
|
|
333
386
|
end
|
|
334
387
|
else
|
|
335
|
-
LOG.info "[Basecamp] Resume: ##{card_number} has #{
|
|
388
|
+
LOG.info "[Basecamp] Resume: ##{card_number} has #{responded_count}/#{total_gates} gate responses, waiting for more reviews" if defined?(LOG)
|
|
336
389
|
end
|
|
337
390
|
end
|
|
338
391
|
end
|