brainiac 0.0.6 → 0.0.7

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +136 -6
  4. data/bin/brainiac +382 -11
  5. data/bin/brainiac-completion.bash +1 -1
  6. data/lib/brainiac/agents.rb +15 -38
  7. data/lib/brainiac/config.rb +40 -11
  8. data/lib/brainiac/handlers/discord/api.rb +196 -0
  9. data/lib/brainiac/handlers/discord/config.rb +134 -0
  10. data/lib/brainiac/handlers/discord/delivery.rb +196 -0
  11. data/lib/brainiac/handlers/discord/gateway.rb +212 -0
  12. data/lib/brainiac/handlers/discord/message.rb +933 -0
  13. data/lib/brainiac/handlers/discord/reactions.rb +215 -0
  14. data/lib/brainiac/handlers/discord.rb +14 -1892
  15. data/lib/brainiac/handlers/fizzy/assignment.rb +125 -0
  16. data/lib/brainiac/handlers/fizzy/comments.rb +730 -0
  17. data/lib/brainiac/handlers/fizzy/dedup.rb +74 -0
  18. data/lib/brainiac/handlers/fizzy/deploy.rb +152 -0
  19. data/lib/brainiac/{deployments.rb → handlers/fizzy/deployments.rb} +4 -2
  20. data/lib/brainiac/handlers/fizzy.rb +12 -1290
  21. data/lib/brainiac/handlers/github.rb +149 -212
  22. data/lib/brainiac/handlers/shared/git.rb +203 -0
  23. data/lib/brainiac/handlers/shared/inline_tags.rb +89 -0
  24. data/lib/brainiac/handlers/zoho.rb +79 -76
  25. data/lib/brainiac/helpers.rb +2 -121
  26. data/lib/brainiac/planning.rb +2 -2
  27. data/lib/brainiac/plugins.rb +129 -0
  28. data/lib/brainiac/restart.rb +94 -0
  29. data/lib/brainiac/routes/api.rb +427 -0
  30. data/lib/brainiac/version.rb +1 -1
  31. data/lib/brainiac/zoho_mail_api.rb +2 -1
  32. data/lib/brainiac.rb +7 -0
  33. data/monitor/daemon.rb +4 -27
  34. data/monitor/shared.rb +247 -0
  35. data/monitor/{waybar-deploy-env.rb → waybar/deploy_env.rb} +14 -86
  36. data/monitor/waybar/setup.rb +232 -0
  37. data/monitor/waybar/status.rb +51 -0
  38. data/monitor/{view-logs-rofi.rb → waybar/view_logs.rb} +21 -88
  39. data/monitor/{deploy-env-macos.rb → xbar/deploy_env.rb} +3 -2
  40. data/monitor/xbar/plugin.rb +155 -0
  41. data/monitor/{setup-menubar.rb → xbar/setup.rb} +14 -30
  42. data/receiver.rb +91 -450
  43. data/templates/brainiac.json.example +9 -0
  44. data/templates/cli-providers/kiro.json.example +8 -2
  45. data/templates/plugins.json.example +3 -0
  46. metadata +30 -20
  47. data/lib/user_registry.rb +0 -159
  48. data/monitor/menubar.rb +0 -295
  49. data/monitor/setup-waybar-deploy-envs.rb +0 -121
  50. data/monitor/setup-waybar-deployments.rb +0 -96
  51. data/monitor/setup-waybar-module.rb +0 -113
  52. data/monitor/setup-xbar-plugin.rb +0 -35
  53. data/monitor/view-logs.rb +0 -119
  54. data/monitor/waybar-config-updater.rb +0 -56
  55. data/monitor/waybar-deployments.rb +0 -239
  56. data/monitor/waybar.rb +0 -146
  57. data/monitor/xbar.3s.rb +0 -179
  58. /data/lib/brainiac/{card_index.rb → handlers/fizzy/card_index.rb} +0 -0
  59. /data/monitor/{open-action.sh → xbar/open_action.sh} +0 -0
  60. /data/monitor/{view-logs-macos.rb → xbar/view_logs.rb} +0 -0
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Card duplicate detection (card_published / card_triaged).
4
+ #
5
+ # When a new card is created, checks for similar existing cards using
6
+ # trigram and semantic similarity. Posts a warning comment if duplicates found.
7
+
8
+ def handle_card_published(payload)
9
+ eventable = payload["eventable"] || {}
10
+ card_number = eventable["number"]
11
+ title = eventable["title"] || ""
12
+ creator_name = payload.dig("creator", "name")
13
+ creator_id = payload.dig("creator", "id")
14
+ tags = eventable["tags"] || []
15
+
16
+ # Creator-based routing: only the machine whose local human created the card
17
+ # handles dedup. Requires `"local": true` on the human in fizzy.json authorized_users.
18
+ local_humans = FIZZY_CONFIG.fetch("authorized_users", []).select { |u| u["human"] && u["local"] }
19
+ if local_humans.empty?
20
+ LOG.info "[CardIndex] No local humans configured — skipping dedup, indexing only"
21
+ return index_card_only(card_number, title, creator_name, creator_id, tags)
22
+ end
23
+
24
+ unless local_humans.any? { |u| u["id"] == creator_id }
25
+ LOG.info "[CardIndex] Ignoring card ##{card_number} — creator '#{creator_name}' is not a local human"
26
+ return index_card_only(card_number, title, creator_name, creator_id, tags)
27
+ end
28
+
29
+ # Check for duplicates before indexing
30
+ similar = CARD_INDEX.find_similar_cards(title, exclude_number: card_number, tags: tags) if card_number
31
+ index_card_only(card_number, title, creator_name, creator_id, tags, skip_response: true)
32
+
33
+ if similar&.any?
34
+ post_duplicate_warning(card_number, title, tags, similar)
35
+ [200, { status: "duplicate_detected", card: card_number,
36
+ similar: similar.map { |s| { number: s[:number], score: s[:score].round(2) } } }.to_json]
37
+ else
38
+ LOG.info "[CardIndex] Card ##{card_number} '#{title}' indexed, no duplicates found"
39
+ [200, { status: "indexed", card: card_number }.to_json]
40
+ end
41
+ end
42
+
43
+ def index_card_only(card_number, title, creator_name, creator_id, tags, skip_response: false)
44
+ CARD_INDEX.index_card(number: card_number, title: title, creator_name: creator_name, creator_id: creator_id, tags: tags) if card_number
45
+ CARD_INDEX.save
46
+ CARD_INDEX.schedule_qmd_reindex
47
+ [200, { status: "indexed", card: card_number }.to_json] unless skip_response
48
+ end
49
+
50
+ def post_duplicate_warning(card_number, title, tags, similar)
51
+ best = similar.first
52
+ LOG.info "[CardIndex] Potential duplicate: ##{card_number} '#{title}' ≈ " \
53
+ "##{best[:number]} '#{best[:title]}' (score: #{best[:score].round(2)})"
54
+
55
+ project_result = identify_project_by_tags(tags)
56
+ return unless project_result
57
+
58
+ _project_key, project_config = project_result
59
+ repo_path = project_config["repo_path"]
60
+
61
+ Thread.new do
62
+ method_label = { trigram: "📝", semantic: "🧠", both: "📝🧠" }
63
+ dupes = similar.map do |s|
64
+ icon = method_label[s[:method]] || "📝"
65
+ "##{s[:number]} \"#{s[:title]}\" (#{(s[:score] * 100).round}% #{icon})"
66
+ end.join("\n- ")
67
+ body = "⚠️ **Possible duplicate detected:**\n- #{dupes}\n\n_📝 = text similarity, 🧠 = semantic similarity_"
68
+ run_cmd("fizzy", "comment", "create", "--card", card_number.to_s, "--body", body,
69
+ chdir: repo_path, env: default_fizzy_env)
70
+ LOG.info "[CardIndex] Posted duplicate warning on card ##{card_number}"
71
+ rescue StandardError => e
72
+ LOG.warn "[CardIndex] Failed to post duplicate warning: #{e.message}"
73
+ end
74
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Fizzy deploy comment handler.
4
+ #
5
+ # When a comment is just "dev02" (or any dev\d+), deploy the card's
6
+ # worktree to that environment. No agent dispatch — reactions only.
7
+
8
+ def handle_deploy_comment(eventable, env_key, card_internal_id)
9
+ comment_id = eventable["id"]
10
+ card_info = load_card_map[card_internal_id]
11
+
12
+ # Validate environment exists
13
+ deploy_config = DEPLOYMENTS_CONFIG["environments"] || {}
14
+ unless deploy_config.key?(env_key)
15
+ LOG.warn "[Deploy] Unknown environment: #{env_key}"
16
+ return [200, { status: "ignored", reason: "unknown environment" }.to_json]
17
+ end
18
+
19
+ # Check environment ownership
20
+ env_owner = deploy_config[env_key]["owner"]
21
+ unless env_owner && env_owner.downcase == AI_AGENT_NAME.downcase
22
+ LOG.info "[Deploy] Skipping #{env_key} — owner is #{env_owner.inspect}, this machine is #{AI_AGENT_NAME}"
23
+ return [200, { status: "ignored", reason: env_owner ? "owned by #{env_owner}" : "no owner configured" }.to_json]
24
+ end
25
+
26
+ worktree = card_info&.dig("worktree")
27
+ card_number = card_info&.dig("number")
28
+
29
+ # If worktree doesn't exist locally, try to clone the branch from origin
30
+ if worktree.nil? || !File.directory?(worktree)
31
+ result = clone_branch_for_deploy(eventable, card_internal_id, card_info)
32
+ unless result
33
+ LOG.warn "[Deploy] Could not resolve or clone branch for card #{card_internal_id}"
34
+ return [200, { status: "ignored", reason: "no worktree and could not clone branch" }.to_json]
35
+ end
36
+ worktree = result[:worktree]
37
+ card_number = result[:card_number]
38
+ end
39
+
40
+ deploy_script = File.join(worktree, "scripts", "deploy.sh")
41
+ unless File.exist?(deploy_script)
42
+ LOG.warn "[Deploy] No deploy script at #{deploy_script}"
43
+ return [200, { status: "ignored", reason: "no deploy script" }.to_json]
44
+ end
45
+
46
+ LOG.info "[Deploy] Deploying card ##{card_number} worktree to #{env_key}"
47
+ mark_deploying(env_key, worktree_path: worktree)
48
+
49
+ Thread.new do
50
+ react_to_deploy(card_number, comment_id, worktree, "🚀")
51
+ run_deploy(env_key, card_number, comment_id, worktree)
52
+ rescue StandardError => e
53
+ LOG.error "[Deploy] Error deploying card ##{card_number} to #{env_key}: #{e.message}"
54
+ react_to_deploy(card_number, comment_id, worktree, "❌")
55
+ end
56
+
57
+ [200, { status: "deploying", card: card_number, env: env_key }.to_json]
58
+ end
59
+
60
+ def react_to_deploy(card_number, comment_id, worktree, emoji)
61
+ run_cmd("fizzy", "reaction", "create", "--card", card_number.to_s,
62
+ "--comment", comment_id.to_s, "--content", emoji,
63
+ chdir: worktree, env: default_fizzy_env)
64
+ rescue StandardError => e
65
+ LOG.warn "[Deploy] Could not add reaction #{emoji}: #{e.message}"
66
+ end
67
+
68
+ def run_deploy(env_key, card_number, comment_id, worktree)
69
+ deploy_env = {}
70
+ aws_profile = DEPLOYMENTS_CONFIG.dig("environments", env_key, "aws_profile")
71
+ deploy_env["AWS_PROFILE"] = aws_profile if aws_profile
72
+
73
+ stdout, stderr, status = Open3.capture3(deploy_env, "./scripts/deploy.sh", env_key, chdir: worktree)
74
+
75
+ if !status.success? && terraform_lock_error?(stdout, stderr)
76
+ stdout, stderr, status = retry_deploy_with_init(deploy_env, env_key, card_number, worktree)
77
+ end
78
+
79
+ if status.success?
80
+ LOG.info "[Deploy] Successfully deployed card ##{card_number} to #{env_key}"
81
+ react_to_deploy(card_number, comment_id, worktree, "✅")
82
+ deploy_to_environment(env_key, worktree_path: worktree, deployed_by: "fizzy-comment")
83
+ else
84
+ LOG.error "[Deploy] Failed deploying card ##{card_number} to #{env_key}: #{stderr}"
85
+ react_to_deploy(card_number, comment_id, worktree, "❌")
86
+ record_deploy_failure(env_key, worktree_path: worktree, stdout: stdout, stderr: stderr)
87
+ end
88
+ end
89
+
90
+ def retry_deploy_with_init(deploy_env, env_key, card_number, worktree)
91
+ LOG.info "[Deploy] Terraform lock file mismatch for card ##{card_number} — retrying with init -upgrade"
92
+ infra_dir = File.join(worktree, "infrastructure", env_key)
93
+ lock_file = File.join(infra_dir, ".terraform.lock.hcl")
94
+ FileUtils.rm_f(lock_file)
95
+ Open3.capture3(deploy_env, "terraform", "init", "-upgrade", chdir: infra_dir) if File.directory?(infra_dir)
96
+ Open3.capture3(deploy_env, "./scripts/deploy.sh", env_key, chdir: worktree)
97
+ end
98
+
99
+ # Clone a remote branch locally for deploy when the worktree doesn't exist on this machine.
100
+ # Returns { worktree:, card_number: } on success, nil on failure.
101
+ def clone_branch_for_deploy(eventable, card_internal_id, card_info)
102
+ card_tags = eventable.dig("card", "tags") || []
103
+ project_result = identify_project_by_tags(card_tags)
104
+ unless project_result
105
+ LOG.warn "[Deploy] Cannot identify project for card #{card_internal_id}"
106
+ return nil
107
+ end
108
+ project_key, project_config = project_result
109
+ repo_path = project_config["repo_path"]
110
+
111
+ card_number = card_info&.dig("number")
112
+ card_number ||= resolve_card_number(card_internal_id, repo_path: repo_path)
113
+ unless card_number
114
+ LOG.warn "[Deploy] Cannot resolve card number for #{card_internal_id}"
115
+ return nil
116
+ end
117
+
118
+ debounced_repo_fetch(repo_path)
119
+ branches = run_cmd("git", "branch", "-r", "--list", "origin/fizzy-#{card_number}-*", chdir: repo_path).strip
120
+ branch = branches.lines.map(&:strip).first&.sub("origin/", "")
121
+ unless branch
122
+ LOG.warn "[Deploy] No remote branch matching fizzy-#{card_number}-* found"
123
+ return nil
124
+ end
125
+
126
+ worktree_path = File.join(File.dirname(repo_path), "#{File.basename(repo_path)}--#{branch}")
127
+
128
+ unless File.directory?(worktree_path)
129
+ branch_exists_locally = system("git", "rev-parse", "--verify", branch, chdir: repo_path, out: File::NULL, err: File::NULL)
130
+ if branch_exists_locally
131
+ run_cmd("git", "worktree", "add", worktree_path, branch, chdir: repo_path)
132
+ else
133
+ run_cmd("git", "worktree", "add", "--track", "-b", branch, worktree_path, "origin/#{branch}", chdir: repo_path)
134
+ end
135
+
136
+ trust_version_manager(worktree_path, chdir: worktree_path)
137
+ apply_worktree_includes(repo_path, worktree_path)
138
+ run_project_hook(repo_path, "worktree-setup", extra_env: { "WORKTREE_PATH" => worktree_path })
139
+ end
140
+
141
+ # Update card map
142
+ map = load_card_map
143
+ map[card_internal_id] ||= {}
144
+ map[card_internal_id].merge!("number" => card_number, "branch" => branch, "worktree" => worktree_path, "project" => project_key)
145
+ save_card_map(map)
146
+
147
+ LOG.info "[Deploy] Cloned branch #{branch} into worktree #{worktree_path} for card ##{card_number}"
148
+ { worktree: worktree_path, card_number: card_number }
149
+ rescue StandardError => e
150
+ LOG.error "[Deploy] Failed to clone branch for card #{card_internal_id}: #{e.message}"
151
+ nil
152
+ end
@@ -81,8 +81,10 @@ def deploy_to_environment(env_key, worktree_path:, deployed_by: nil)
81
81
  entry["pr_url"] = pr["url"]
82
82
  end
83
83
  # Store card tags for URL resolution (e.g. ops-web-app → ops URL)
84
- card_idx = CARD_INDEX[card_entry["number"].to_s]
85
- entry["card_tags"] = card_idx["tags"] if card_idx && card_idx["tags"]
84
+ if defined?(CARD_INDEX)
85
+ card_idx = CARD_INDEX[card_entry["number"].to_s]
86
+ entry["card_tags"] = card_idx["tags"] if card_idx && card_idx["tags"]
87
+ end
86
88
  else
87
89
  # No card map match — record branch from git
88
90
  branch = `git -C #{Shellwords.escape(worktree_path)} rev-parse --abbrev-ref HEAD 2>/dev/null`.strip