brainiac-basecamp 0.0.24 → 0.0.26

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: 5c8050e48cf49fc743e8e1cf01db98abcb279fa7b8a2c70b99ee77cf2f331378
4
- data.tar.gz: 0eb5638888a2369c61b1b0cc3bfed7c4729df1dc1ed2e14e56af69ca36280644
3
+ metadata.gz: 527e9e3b326e216a5080ae89148317e15530ac84d07ec01e30a90a3676528f9c
4
+ data.tar.gz: 436a083dda6818c8328df04179f3789d9febc0f4f6bd3016f744d7cd09921bc8
5
5
  SHA512:
6
- metadata.gz: 8aca50da9477027ff6800582dea934506a76897256eb4c91d8f2a9cfd8f27b3c786784a98410f487eb9c77d83924a74ee9ea9e753fe8d21c2f57c885afd7de30
7
- data.tar.gz: c64f4de4ff526fa7c0a18149309b61a917b04f373ea02bc31855b752b9ce0ad820df9b040b90fe71d4fdcd2d5f7221e2ef5d3f297b6a90747cbe738b632f667f
6
+ metadata.gz: 5cfd4aba7b96d8fdc49c368dab4dc76c8f21f2a29052ff6f8271dcee4cc824e95f9036aeb0325a540c9c082194b9d3854705092629ea9020fb16797ffe3d413f
7
+ data.tar.gz: 97ee914315cd7f978a3b736553db059e572a3bd629950cc7ed7220d644bdf6b6d89bda0d013e317a87b9b70c21069e7b4e3776563b82f20e062f59c67f260bfa
@@ -986,9 +986,15 @@ module Brainiac
986
986
  # Reset the task
987
987
  reset_task_to(task, target_status)
988
988
 
989
+ # If completing this task leaves every task in the epic complete,
990
+ # seal the epic in the same atomic write so the reconcile loop never
991
+ # re-fires the completion notification.
992
+ finalized = finalize_epic_if_all_complete!(epic)
993
+
989
994
  save_epics(epics)
990
995
  puts "✓ Reset card ##{card_number} from '#{old_status}' → '#{target_status}'"
991
996
  puts " Epic: #{epic['title']}"
997
+ puts "✓ All tasks complete — epic finalized (status=complete, notification suppressed)" if finalized
992
998
 
993
999
  return unless target_status == "pending"
994
1000
 
@@ -1096,8 +1102,11 @@ module Brainiac
1096
1102
  tasks_to_reset.each { |t| reset_task_to(t, target_status) }
1097
1103
  epic["updated_at"] = Time.now.iso8601
1098
1104
 
1105
+ finalized = finalize_epic_if_all_complete!(epic)
1106
+
1099
1107
  save_epics(epics)
1100
1108
  puts "✓ Reset #{tasks_to_reset.size} task(s) → '#{target_status}' in epic '#{epic['title']}'"
1109
+ puts "✓ All tasks complete — epic finalized (status=complete, notification suppressed)" if finalized
1101
1110
  puts ""
1102
1111
  epic["tasks"].each do |t|
1103
1112
  icon = case t["status"]
@@ -1169,9 +1178,31 @@ module Brainiac
1169
1178
  task["awaiting_final_decision"] = true
1170
1179
  when "complete"
1171
1180
  task["completed_at"] ||= Time.now.iso8601
1181
+ # A completed task is no longer awaiting a final decision. Leaving
1182
+ # this true lets reconcile_final_decision_task treat the task as
1183
+ # unsettled and re-dispatch/re-complete it on the next sweep.
1184
+ task["awaiting_final_decision"] = false
1172
1185
  end
1173
1186
  end
1174
1187
 
1188
+ # Seal an epic when a reset leaves every task complete. Without this, an
1189
+ # epic can sit status="active" with all tasks done — exactly the
1190
+ # thrash-prone state where the 90s reconcile loop re-detects "all
1191
+ # complete", re-runs complete_epic, and re-posts the "🎉 Epic completed"
1192
+ # notification on every sweep. Setting completion_notified suppresses a
1193
+ # notification for this manual, operator-driven completion. Returns true
1194
+ # if the epic was finalized.
1195
+ def finalize_epic_if_all_complete!(epic)
1196
+ return false unless epic["tasks"]&.any?
1197
+ return false unless epic["tasks"].all? { |t| t["status"] == "complete" }
1198
+ return false if epic["status"] == "complete"
1199
+
1200
+ epic["status"] = "complete"
1201
+ epic["completed_at"] ||= Time.now.iso8601
1202
+ epic["completion_notified"] = true
1203
+ true
1204
+ end
1205
+
1175
1206
  def load_epics
1176
1207
  epics_file = File.join(BRAINIAC_DIR, "basecamp_epics.json")
1177
1208
  return [] unless File.exist?(epics_file)
@@ -790,12 +790,12 @@ module Brainiac
790
790
  return unless project_config && project_config["repo_path"]
791
791
 
792
792
  agent_name = Orchestrator.send(:resolve_agent_for_project, project_key) || epic["agent"]
793
- prompt = <<~PROMPT
794
- ## Changes Requested Fizzy Card ##{card_number}
795
-
796
- Review feedback needs attention on PR ##{task['pr_number']}. Read the review,
797
- make the requested fixes, commit and push them, then update the Fizzy card.
798
- PROMPT
793
+ # Only build a "changes requested" prompt when there is a real PR to
794
+ # act on. A stale in_flight card recovered by the orchestrator has no
795
+ # PR number yet — handing an agent "...fixes on PR #" with a blank
796
+ # number is nonsense and the session dies asking for clarification.
797
+ # In that case dispatch a clean "start implementation" prompt instead.
798
+ prompt = build_impl_prompt(card_number: card_number, pr_number: task["pr_number"])
799
799
  github_repo = project_config["github_repo"]
800
800
  agent_env = github_repo ? ReviewGate.send(:resolve_agent_github_env, agent_name, github_repo) : {}
801
801
  pid, log_file = method(:run_agent).call(
@@ -824,6 +824,36 @@ module Brainiac
824
824
  LOG.error "[Basecamp:Hooks] Direct implementation dispatch failed: #{e.message}" if defined?(LOG)
825
825
  end
826
826
 
827
+ # Build the implementation prompt for a direct dispatch.
828
+ #
829
+ # When +pr_number+ is present the card already has an open PR, so the
830
+ # agent is asked to address review feedback. When it is absent (a stale
831
+ # in_flight card the orchestrator recovered, or a first dispatch) the
832
+ # agent is asked to start the implementation from scratch — never with a
833
+ # blank "PR #" that a real PR would have filled in.
834
+ #
835
+ # @param card_number [Integer, String] Fizzy card number
836
+ # @param pr_number [Integer, String, nil] PR number, if one exists
837
+ # @return [String] the prompt text
838
+ def build_impl_prompt(card_number:, pr_number:)
839
+ if pr_number.to_s.strip.empty?
840
+ <<~PROMPT
841
+ ## Implement — Fizzy Card ##{card_number}
842
+
843
+ Pick up Fizzy card ##{card_number} and implement it. Read the card for
844
+ requirements, do the work in a branch, commit and push, open a PR, then
845
+ update the Fizzy card. There is no existing PR for this card yet.
846
+ PROMPT
847
+ else
848
+ <<~PROMPT
849
+ ## Changes Requested — Fizzy Card ##{card_number}
850
+
851
+ Review feedback needs attention on PR ##{pr_number}. Read the review,
852
+ make the requested fixes, commit and push them, then update the Fizzy card.
853
+ PROMPT
854
+ end
855
+ end
856
+
827
857
  # Find a PR number by branch name pattern.
828
858
  def find_pr_number(repo_path:, branch:)
829
859
  # Try exact match first
@@ -593,6 +593,25 @@ module Brainiac
593
593
  nil
594
594
  end
595
595
 
596
+ # Whether a card's completion is backed by real evidence of shipped work.
597
+ #
598
+ # A card genuinely completes when it has an associated PR that merged (or,
599
+ # at minimum, a tracked PR number). Without any PR evidence the completion
600
+ # is almost certainly spurious — a stale ledger flag left behind by a
601
+ # basecamp re-sync that dropped merged cards — and we should not treat it
602
+ # as a real completion that warrants an epic-review checkpoint.
603
+ #
604
+ # @param epic [Hash] epic state
605
+ # @param card_number [Integer, String] the card claimed complete
606
+ # @return [Boolean]
607
+ def card_completion_verified?(epic, card_number)
608
+ task = epic["tasks"].find { |t| t["fizzy_card"] == card_number.to_i }
609
+ return false unless task
610
+
611
+ # A tracked PR number is our recorded evidence that work shipped.
612
+ !task["pr_number"].to_s.strip.empty?
613
+ end
614
+
596
615
  # Dispatch an agent to review the epic state after a task completes.
597
616
  # This ensures the remaining plan still makes sense given implementation decisions.
598
617
  # The callback is called after the review completes.
@@ -606,6 +625,22 @@ module Brainiac
606
625
  return
607
626
  end
608
627
 
628
+ # Only post a "just completed" checkpoint when the card actually shipped
629
+ # work. A ledger flagged `complete` with no PR (and no merge) means the
630
+ # completion was spurious — usually the basecamp re-sync dropping merged
631
+ # cards and leaving a stale flag. Posting "Card #N just completed" in that
632
+ # case is a false claim that spawns a pointless epic-review reflex.
633
+ # Still run the callback so the resolver can advance (it correctly does
634
+ # nothing for cards that remain blocked).
635
+ unless card_completion_verified?(epic, completed_card_number)
636
+ if defined?(LOG)
637
+ LOG.warn "[Basecamp:Orchestrator] Skipping epic review after card " \
638
+ "##{completed_card_number} — no PR/merge evidence of completion"
639
+ end
640
+ callback&.call
641
+ return
642
+ end
643
+
609
644
  LOG.info "[Basecamp:Orchestrator] Dispatching epic review after card ##{completed_card_number}" if defined?(LOG)
610
645
 
611
646
  # Ensure epic memory exists (in case this epic started before the feature)
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Basecamp
6
- VERSION = "0.0.24"
6
+ VERSION = "0.0.26"
7
7
  end
8
8
  end
9
9
  end
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.24
4
+ version: 0.0.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis