brainiac-basecamp 0.0.12 → 0.0.14

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: 1eb44286a14e4c833e7ee753cf3ced44e6579eea0d37ff0c1289db149cc1c97c
4
- data.tar.gz: b24e2ed8efe6a611f3d3790dfd38a906cd753c8c766743ab50302f365430d8db
3
+ metadata.gz: 14642b9582bda42dff7932fe0b518957bdeb638431de432df6beae906c3206e5
4
+ data.tar.gz: 2fac38d27c6c89445d49af1b8e3148bcfa25d8d6632de11984fca1dbd772dcf2
5
5
  SHA512:
6
- metadata.gz: 106c01e99ca88a0ee5d2801cb632b17fdcace3bd3ea9fe17278cae2cbfa3560f1669c164bf6a12472fc4451bdc81833889aaad2c273b936df77ead50f1736450
7
- data.tar.gz: f766419152f38af5404a7640ac87c19540814480e67a1e12ee14621595075d0fc7fc204ed33475022a090c74fbfa97ab8b5a3929c9eedfe3458b2f46345a7fa0
6
+ metadata.gz: f1c4b21c180b3a8306770e922c8c4c8bdb65976a1a62f9dbe02541cb4049d635ac4745f469275ad13bd1168811148cb772965e0b64e4d5b0fdac9b0eb4b7c200
7
+ data.tar.gz: 7a063d983c097531096ff5d8f954050842f0b775f136f6ec1694111ab0bd9561f061b859e33a9497a7b69b8ed7e3a1aa0725d7c70895556e83789920fedc1f72
@@ -156,10 +156,75 @@ module Brainiac
156
156
  # If project is set, look up directly
157
157
  return epic_branches[project_key] if project_key && epic_branches[project_key]
158
158
 
159
- # Fallback: if there's only one epic branch, use it (single-project epic)
160
- return epic_branches.values.first if epic_branches.size == 1
159
+ # Fallback: if there's only one epic branch AND the task has no specific project set,
160
+ # use it (assumed single-project epic).
161
+ # Do NOT fallback if the task has a specific project that isn't in epic_branches —
162
+ # that means the branch wasn't created for this repo and we should return nil
163
+ # (letting the default branch be used) rather than returning a branch from a different repo.
164
+ return epic_branches.values.first if epic_branches.size == 1 && !project_key
165
+
166
+ # Multi-project epic but no matching branch for this task's project — return nil
167
+ nil
168
+ end
169
+
170
+ # Lazily ensure an epic branch exists for a card's project.
171
+ # Called when epic_branch_for_card returns nil but the card IS in an active epic.
172
+ # Creates the branch in the task's repo and registers it in epic state.
173
+ #
174
+ # @param card_number [Integer, String] Fizzy card number
175
+ # @return [String, nil] Epic branch name or nil
176
+ def ensure_epic_branch_for_card(card_number)
177
+ epic = Orchestrator.find_epic_for_card(card_number.to_i)
178
+ return nil unless epic
179
+ return nil unless epic["review_gate"] == "epic_branch"
180
+
181
+ task = epic["tasks"]&.find { |t| t["fizzy_card"] == card_number.to_i }
182
+ return nil unless task
183
+
184
+ project_key = task["project"]
185
+ return nil unless project_key
161
186
 
162
- # Multi-project epic but no project on task — can't determine which branch
187
+ # Already have a branch for this project
188
+ epic_branches = epic["epic_branches"] || {}
189
+ return epic_branches[project_key] if epic_branches[project_key]
190
+
191
+ # Resolve repo path for this project
192
+ projects_file = File.join(BRAINIAC_DIR, "projects.json")
193
+ return nil unless File.exist?(projects_file)
194
+
195
+ all_projects = JSON.parse(File.read(projects_file))
196
+ repo_path = all_projects.dig(project_key, "repo_path")
197
+ return nil unless repo_path
198
+
199
+ # Create the branch
200
+ slug = branch_slug(epic["title"])
201
+ branch_name = "epic/#{slug}"
202
+ default_branch = detect_default_branch(repo_path)
203
+
204
+ run_git("fetch", "origin", chdir: repo_path)
205
+
206
+ # Check if branch already exists remotely
207
+ remote_exists = system("git", "ls-remote", "--exit-code", "--heads", "origin", branch_name,
208
+ chdir: repo_path, out: File::NULL, err: File::NULL)
209
+
210
+ if remote_exists
211
+ run_git("fetch", "origin", "#{branch_name}:#{branch_name}", chdir: repo_path)
212
+ LOG.info "[Basecamp:EpicBranch] Lazy-created: reusing existing '#{branch_name}' in #{project_key}" if defined?(LOG)
213
+ else
214
+ run_git("branch", branch_name, "origin/#{default_branch}", chdir: repo_path)
215
+ run_git("push", "-u", "origin", branch_name, chdir: repo_path)
216
+ LOG.info "[Basecamp:EpicBranch] Lazy-created: new '#{branch_name}' in #{project_key}" if defined?(LOG)
217
+ end
218
+
219
+ # Register in epic state
220
+ epic["epic_branches"] ||= {}
221
+ epic["epic_branches"][project_key] = branch_name
222
+ epic["updated_at"] = Time.now.iso8601
223
+ Orchestrator.send(:save_epic, epic)
224
+
225
+ branch_name
226
+ rescue StandardError => e
227
+ LOG.error "[Basecamp:EpicBranch] Lazy branch creation failed for #{project_key}: #{e.message}" if defined?(LOG)
163
228
  nil
164
229
  end
165
230
 
@@ -482,12 +482,17 @@ module Brainiac
482
482
  end
483
483
 
484
484
  # Return the epic branch as the worktree base for cards in an active epic.
485
+ # If the epic branch doesn't exist for this task's project yet, create it lazily.
485
486
  def register_resolve_base_branch
486
487
  Brainiac.on(:resolve_base_branch) do |ctx|
487
488
  card_number = ctx[:card_number]
488
489
  next unless card_number
489
490
 
490
491
  branch = EpicBranch.epic_branch_for_card(card_number)
492
+
493
+ # Lazy creation: if no branch exists for this task's project, create one now
494
+ branch ||= EpicBranch.ensure_epic_branch_for_card(card_number)
495
+
491
496
  next unless branch
492
497
 
493
498
  "origin/#{branch}"
@@ -495,12 +500,15 @@ module Brainiac
495
500
  end
496
501
 
497
502
  # Return the epic branch as the PR target for cards in an active epic.
503
+ # Same lazy-creation logic as resolve_base_branch.
498
504
  def register_resolve_pr_target
499
505
  Brainiac.on(:resolve_pr_target) do |ctx|
500
506
  card_number = ctx[:card_number]
501
507
  next unless card_number
502
508
 
503
- EpicBranch.epic_branch_for_card(card_number)
509
+ branch = EpicBranch.epic_branch_for_card(card_number)
510
+ branch ||= EpicBranch.ensure_epic_branch_for_card(card_number)
511
+ branch
504
512
  end
505
513
  end
506
514
 
@@ -189,6 +189,8 @@ module Brainiac
189
189
  load_epics.find { |e| e["id"] == epic_id }
190
190
  end
191
191
 
192
+ WEBHOOK_CHECK_COOLDOWN = 300 # 5 minutes
193
+
192
194
  private
193
195
 
194
196
  # Resolve current todolist state from Basecamp and dispatch unblocked cards.
@@ -264,7 +266,9 @@ module Brainiac
264
266
  end
265
267
 
266
268
  # Dispatch unblocked tasks that aren't already in-flight or complete.
269
+ # Ensures Fizzy webhooks are active before dispatching (prevents silent failures).
267
270
  def dispatch_unblocked_tasks(epic)
271
+ ensure_fizzy_webhooks_active
268
272
  tasks = epic["tasks"].map do |t|
269
273
  Epic::Task.new(
270
274
  todo_id: t["todo_id"],
@@ -316,7 +320,9 @@ module Brainiac
316
320
 
317
321
  log_event(epic, "dispatched", "Card ##{card_number} dispatched to #{agent}")
318
322
 
319
- # Assign the card in Fizzy via CLI — this triggers the normal Fizzy webhook flow
323
+ # Assign the card in Fizzy via CLI — this triggers the normal Fizzy webhook flow.
324
+ # If the agent is already assigned (stale state from a disabled webhook),
325
+ # the method will cycle the assignment to re-trigger the webhook.
320
326
  assign_fizzy_card(card_number, agent)
321
327
 
322
328
  # Post a comment on the Basecamp todo
@@ -416,7 +422,10 @@ module Brainiac
416
422
  end
417
423
 
418
424
  # Assign a Fizzy card to an agent via Fizzy CLI.
419
- # If the agent is already assigned, skip the webhook should have already fired.
425
+ # If the agent is already assigned, unassigns and reassigns to re-trigger the webhook
426
+ # (Fizzy webhooks can silently become disabled, causing the original assignment to be lost).
427
+ #
428
+ # @return [Symbol] :assigned, :reassigned, or :failed
420
429
  def assign_fizzy_card(card_number, agent)
421
430
  agent_config = load_agent_registry[agent.downcase]
422
431
  fizzy_name = agent_config&.dig("fizzy_name") || agent
@@ -425,18 +434,34 @@ module Brainiac
425
434
  fizzy_user_id = resolve_fizzy_user_id(fizzy_name)
426
435
  unless fizzy_user_id
427
436
  LOG.error "[Basecamp:Orchestrator] Could not resolve Fizzy user ID for '#{fizzy_name}'" if defined?(LOG)
428
- return
437
+ return :failed
429
438
  end
430
439
 
431
- # Check if agent is already assigned — if so, skip (webhook should have fired)
440
+ # Check if agent is already assigned
432
441
  stdout, _, status = Open3.capture3("fizzy", "card", "show", card_number.to_s, "--json")
433
442
  if status.success?
434
443
  card_data = JSON.parse(stdout).dig("data") rescue nil
435
444
  if card_data
436
445
  current_assignees = (card_data["assignees"] || []).map { |a| a["id"] }
437
446
  if current_assignees.include?(fizzy_user_id)
438
- LOG.info "[Basecamp:Orchestrator] Agent already assigned to ##{card_number}, skipping (webhook should have fired)" if defined?(LOG)
439
- return
447
+ # Agent is already assigned webhook may not have fired (disabled webhook).
448
+ # Unassign then reassign to trigger a fresh webhook delivery.
449
+ LOG.warn "[Basecamp:Orchestrator] Agent already assigned to ##{card_number} — " \
450
+ "cycling assignment to re-trigger webhook" if defined?(LOG)
451
+
452
+ # Fizzy assign TOGGLES, so calling it removes the assignment
453
+ Open3.capture3("fizzy", "card", "assign", card_number.to_s, "--user", fizzy_user_id)
454
+ sleep 1 # Brief pause to ensure the unassign is processed
455
+
456
+ # Now reassign — this should trigger the webhook
457
+ stdout2, stderr2, status2 = Open3.capture3("fizzy", "card", "assign", card_number.to_s, "--user", fizzy_user_id)
458
+ if status2.success?
459
+ LOG.info "[Basecamp:Orchestrator] Reassigned Fizzy ##{card_number} to #{fizzy_name} (webhook re-triggered)" if defined?(LOG)
460
+ return :reassigned
461
+ else
462
+ LOG.error "[Basecamp:Orchestrator] Failed to reassign Fizzy ##{card_number}: #{stderr2.strip}" if defined?(LOG)
463
+ return :failed
464
+ end
440
465
  end
441
466
  end
442
467
  end
@@ -445,11 +470,77 @@ module Brainiac
445
470
 
446
471
  if status.success?
447
472
  LOG.info "[Basecamp:Orchestrator] Assigned Fizzy ##{card_number} to #{fizzy_name} (#{fizzy_user_id})" if defined?(LOG)
473
+ :assigned
448
474
  else
449
475
  LOG.error "[Basecamp:Orchestrator] Failed to assign Fizzy ##{card_number}: #{stderr.strip}" if defined?(LOG)
476
+ :failed
450
477
  end
451
478
  rescue Errno::ENOENT => e
452
479
  LOG.error "[Basecamp:Orchestrator] fizzy CLI not found: #{e.message}" if defined?(LOG)
480
+ :failed
481
+ end
482
+
483
+ # Ensure Fizzy webhooks are active before dispatching cards.
484
+ # Fizzy webhooks can silently become disabled (e.g., after repeated delivery failures),
485
+ # causing assigned cards to never trigger agent work. This pre-flight check reactivates
486
+ # any disabled brainiac webhooks.
487
+ #
488
+ # Uses a cooldown to avoid hammering the API on every dispatch batch.
489
+ def ensure_fizzy_webhooks_active
490
+ now = Time.now
491
+ @last_webhook_check ||= Time.at(0)
492
+ return if (now - @last_webhook_check) < WEBHOOK_CHECK_COOLDOWN
493
+
494
+ @last_webhook_check = now
495
+
496
+ # Read fizzy config to get admin_token and board list
497
+ fizzy_config_file = File.join(BRAINIAC_DIR, "fizzy.json")
498
+ return unless File.exist?(fizzy_config_file)
499
+
500
+ fizzy_config = JSON.parse(File.read(fizzy_config_file))
501
+ admin_token = fizzy_config["admin_token"]
502
+ boards = fizzy_config["boards"] || {}
503
+
504
+ unless admin_token
505
+ LOG.debug "[Basecamp:Orchestrator] Fizzy webhook check skipped (no admin_token in fizzy.json)" if defined?(LOG)
506
+ return
507
+ end
508
+
509
+ env = { "FIZZY_TOKEN" => admin_token }
510
+ reactivated = 0
511
+
512
+ boards.each do |board_key, board_config|
513
+ board_id = board_config["board_id"]
514
+ next unless board_id
515
+
516
+ stdout, _, status = Open3.capture3(env, "fizzy", "webhook", "list",
517
+ "--board", board_id, "--all", "--json",
518
+ chdir: Dir.home)
519
+ next unless status.success?
520
+
521
+ webhooks = JSON.parse(stdout)["data"] || []
522
+ webhooks.each do |webhook|
523
+ next unless webhook["name"]&.start_with?("brainiac")
524
+ next if webhook["active"]
525
+
526
+ wh_name = webhook["name"]
527
+ LOG.warn "[Basecamp:Orchestrator] Fizzy webhook '#{wh_name}' on board '#{board_key}' is INACTIVE — reactivating" if defined?(LOG)
528
+ _, _, reactivate_status = Open3.capture3(env, "fizzy", "webhook", "reactivate", webhook["id"],
529
+ "--board", board_id, chdir: Dir.home)
530
+ if reactivate_status.success?
531
+ reactivated += 1
532
+ LOG.info "[Basecamp:Orchestrator] Reactivated webhook '#{webhook['name']}' on board '#{board_key}'" if defined?(LOG)
533
+ else
534
+ LOG.error "[Basecamp:Orchestrator] Failed to reactivate webhook '#{webhook['name']}' on board '#{board_key}'" if defined?(LOG)
535
+ end
536
+ end
537
+ rescue StandardError => e
538
+ LOG.warn "[Basecamp:Orchestrator] Webhook check failed for board '#{board_key}': #{e.message}" if defined?(LOG)
539
+ end
540
+
541
+ LOG.info "[Basecamp:Orchestrator] Fizzy webhook health check: #{reactivated} reactivated" if reactivated.positive?
542
+ rescue StandardError => e
543
+ LOG.warn "[Basecamp:Orchestrator] Fizzy webhook health check failed: #{e.message}" if defined?(LOG)
453
544
  end
454
545
 
455
546
  # Resolve a Fizzy user ID from their display name.
@@ -631,6 +722,15 @@ module Brainiac
631
722
  profile: epic["agent"]&.downcase)
632
723
  end
633
724
 
725
+ # Mark a Basecamp todo as incomplete (undo premature completion).
726
+ def uncheck_todo(epic, card_number)
727
+ task = epic["tasks"].find { |t| t["fizzy_card"] == card_number.to_i }
728
+ return unless task && task["todo_id"]
729
+
730
+ Client.run_safe("todos", "uncomplete", task["todo_id"].to_s, "--json",
731
+ profile: epic["agent"]&.downcase)
732
+ end
733
+
634
734
  # Post a completion comment on the Basecamp todo.
635
735
  def post_completion_comment(epic, card_number)
636
736
  task = epic["tasks"].find { |t| t["fizzy_card"] == card_number.to_i }
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Basecamp
6
- VERSION = "0.0.12"
6
+ VERSION = "0.0.14"
7
7
  end
8
8
  end
9
9
  end
@@ -160,7 +160,19 @@ module Brainiac
160
160
 
161
161
  when "in_review"
162
162
  # Check if all gates have actually approved (webhook might have missed)
163
- if pr_number && project_key && ReviewGate.enabled?
163
+ # Resolve pr_number if missing/zero (same pattern as final_decision)
164
+ effective_pr = pr_number
165
+ if (effective_pr.nil? || effective_pr.zero?) && project_key
166
+ resolved = resolve_pr_for_task(task)
167
+ if resolved
168
+ task["pr_number"] = resolved[:number]
169
+ task["pr_repo"] = resolved[:repo] if resolved[:repo]
170
+ effective_pr = resolved[:number]
171
+ LOG.info "[Basecamp:HealthCheck] Resolved PR ##{effective_pr} for in_review card ##{card_number}" if defined?(LOG)
172
+ end
173
+ end
174
+
175
+ if effective_pr&.positive? && project_key && ReviewGate.enabled?
164
176
  projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
165
177
  projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
166
178
  repo_path = projects.dig(project_key, "repo_path")
@@ -178,6 +190,28 @@ module Brainiac
178
190
  Hooks.send(:save_epic_state, epic)
179
191
  Hooks.send(:dispatch_final_decision, epic, task, {})
180
192
  healed_any = true
193
+ elsif task["changes_requested_by"]&.any? && pr_has_newer_commits_than_reviews?(task, repo_path: repo_path)
194
+ # Changes were requested but the impl agent has already pushed fixes.
195
+ # The pr_synchronized hook missed this (pr_number was 0, or status wasn't in_flight).
196
+ # Reset approvals and re-dispatch gates to review the new code.
197
+ LOG.info "[Basecamp:HealthCheck] PR has commits newer than last changes_requested review for ##{card_number} — re-dispatching gates" if defined?(LOG)
198
+ ReviewGate.reset_approvals(task)
199
+ task["changes_requested_by"] = []
200
+ task["gates_dispatched_at"] = Time.now.iso8601
201
+ Hooks.send(:save_epic_state, epic)
202
+
203
+ Thread.new do
204
+ ReviewGate.dispatch_gates(
205
+ epic: epic,
206
+ task: task,
207
+ pr_number: effective_pr,
208
+ repo_name: github_repo,
209
+ repo_path: repo_path
210
+ )
211
+ rescue StandardError => e
212
+ LOG.error "[Basecamp:HealthCheck] Gate re-dispatch failed for ##{card_number}: #{e.message}" if defined?(LOG)
213
+ end
214
+ healed_any = true
181
215
  elsif Hooks.send(:all_gates_responded?, task) && task["changes_requested_by"]&.any?
182
216
  # All gates responded but some requested changes — need to dispatch impl agent
183
217
  impl_agent = epic["agent"]
@@ -208,7 +242,7 @@ module Brainiac
208
242
  ReviewGate.dispatch_gates(
209
243
  epic: epic,
210
244
  task: task,
211
- pr_number: pr_number,
245
+ pr_number: effective_pr,
212
246
  repo_name: github_repo,
213
247
  repo_path: repo_path
214
248
  )
@@ -226,7 +260,7 @@ module Brainiac
226
260
  ReviewGate.dispatch_missing_gates(
227
261
  epic: epic,
228
262
  task: task,
229
- pr_number: pr_number,
263
+ pr_number: effective_pr,
230
264
  repo_name: github_repo,
231
265
  repo_path: repo_path
232
266
  )
@@ -237,6 +271,65 @@ module Brainiac
237
271
  end
238
272
  end
239
273
  end
274
+
275
+ when "complete"
276
+ # Sanity check: verify the PR is actually merged before accepting "complete" status.
277
+ # Catches race conditions where rapid review cycles cause premature task completion.
278
+ next unless project_key
279
+
280
+ effective_pr = pr_number
281
+ if effective_pr.nil? || effective_pr.zero?
282
+ resolved = resolve_pr_for_task(task)
283
+ if resolved
284
+ task["pr_number"] = resolved[:number]
285
+ task["pr_repo"] = resolved[:repo] if resolved[:repo]
286
+ effective_pr = resolved[:number]
287
+ end
288
+ end
289
+
290
+ # If we can't find a PR, we can't verify — skip (might be a no-code task)
291
+ next unless effective_pr&.positive?
292
+
293
+ projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
294
+ projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
295
+ github_repo = projects.dig(project_key, "github_repo")
296
+ repo_path = projects.dig(project_key, "repo_path")
297
+ next unless github_repo
298
+
299
+ pr_state, = Open3.capture2("gh", "pr", "view", effective_pr.to_s, "--repo", github_repo, "--json", "state", "-q", ".state")
300
+ pr_state = pr_state.strip
301
+
302
+ if pr_state == "OPEN"
303
+ # PR is NOT merged — task was prematurely marked complete.
304
+ # Revert to in_review and re-dispatch gates to review current state.
305
+ LOG.info "[Basecamp:HealthCheck] Task ##{card_number} marked complete but PR ##{effective_pr} is still OPEN — reverting to in_review" if defined?(LOG)
306
+
307
+ task["status"] = "in_review"
308
+ task.delete("completed_at")
309
+ task["gate_approvals"] = []
310
+ task["changes_requested_by"] = []
311
+ task["gates_dispatched_at"] = Time.now.iso8601
312
+ Hooks.send(:save_epic_state, epic)
313
+
314
+ # Uncheck the Basecamp todo (it was incorrectly checked)
315
+ Orchestrator.send(:uncheck_todo, epic, card_number) if Orchestrator.respond_to?(:uncheck_todo, true)
316
+
317
+ # Re-dispatch review gates
318
+ if ReviewGate.enabled? && repo_path
319
+ Thread.new do
320
+ ReviewGate.dispatch_gates(
321
+ epic: epic,
322
+ task: task,
323
+ pr_number: effective_pr,
324
+ repo_name: github_repo,
325
+ repo_path: repo_path
326
+ )
327
+ rescue StandardError => e
328
+ LOG.error "[Basecamp:HealthCheck] Gate dispatch failed for ##{card_number}: #{e.message}" if defined?(LOG)
329
+ end
330
+ end
331
+ healed_any = true
332
+ end
240
333
  end
241
334
  end
242
335
 
@@ -249,6 +342,36 @@ module Brainiac
249
342
 
250
343
  private
251
344
 
345
+ # Check if a PR has commits newer than the most recent changes_requested review.
346
+ # This detects when the impl agent pushed fixes but gates weren't re-triggered.
347
+ #
348
+ # @param task [Hash] Task state with pr_number
349
+ # @param repo_path [String] Local repo path for gh CLI
350
+ # @return [Boolean] true if PR has commits after the last changes_requested review
351
+ def pr_has_newer_commits_than_reviews?(task, repo_path:)
352
+ pr_number = task["pr_number"]
353
+ return false unless pr_number&.positive?
354
+
355
+ # Get the last changes_requested review timestamp and the latest commit timestamp
356
+ stdout, _, status = Open3.capture3(
357
+ "gh", "pr", "view", pr_number.to_s,
358
+ "--json", "reviews,commits",
359
+ "--jq", "[(.reviews | map(select(.state == \"CHANGES_REQUESTED\")) | sort_by(.submittedAt) | last | .submittedAt), (.commits | last | .committedDate)] | @tsv",
360
+ chdir: repo_path
361
+ )
362
+ return false unless status.success? && !stdout.strip.empty?
363
+
364
+ last_changes_at, last_commit_at = stdout.strip.split("\t")
365
+ return false if last_changes_at.nil? || last_changes_at.empty? || last_commit_at.nil? || last_commit_at.empty?
366
+
367
+ # Compare timestamps — if the latest commit is newer than the last changes_requested review,
368
+ # the impl agent has pushed fixes that haven't been reviewed yet.
369
+ Time.parse(last_commit_at) > Time.parse(last_changes_at)
370
+ rescue StandardError => e
371
+ LOG.warn "[Basecamp:HealthCheck] Error checking PR commits vs reviews: #{e.message}" if defined?(LOG)
372
+ false
373
+ end
374
+
252
375
  # Resume active epics on server startup.
253
376
  # For each epic, checks task states and takes appropriate action.
254
377
  def resume_active_epics(epics)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac-basecamp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.12
4
+ version: 0.0.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis