brainiac 0.0.5 → 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.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +136 -6
- data/bin/brainiac +385 -19
- data/bin/brainiac-completion.bash +1 -1
- data/lib/brainiac/agents.rb +15 -38
- data/lib/brainiac/config.rb +40 -11
- data/lib/brainiac/handlers/discord/api.rb +196 -0
- data/lib/brainiac/handlers/discord/config.rb +134 -0
- data/lib/brainiac/handlers/discord/delivery.rb +196 -0
- data/lib/brainiac/handlers/discord/gateway.rb +212 -0
- data/lib/brainiac/handlers/discord/message.rb +933 -0
- data/lib/brainiac/handlers/discord/reactions.rb +215 -0
- data/lib/brainiac/handlers/discord.rb +14 -1892
- data/lib/brainiac/handlers/fizzy/assignment.rb +125 -0
- data/lib/brainiac/handlers/fizzy/comments.rb +730 -0
- data/lib/brainiac/handlers/fizzy/dedup.rb +74 -0
- data/lib/brainiac/handlers/fizzy/deploy.rb +152 -0
- data/lib/brainiac/{deployments.rb → handlers/fizzy/deployments.rb} +4 -2
- data/lib/brainiac/handlers/fizzy.rb +12 -1290
- data/lib/brainiac/handlers/github.rb +149 -212
- data/lib/brainiac/handlers/shared/git.rb +203 -0
- data/lib/brainiac/handlers/shared/inline_tags.rb +89 -0
- data/lib/brainiac/handlers/zoho.rb +79 -76
- data/lib/brainiac/helpers.rb +2 -121
- data/lib/brainiac/planning.rb +2 -2
- data/lib/brainiac/plugins.rb +129 -0
- data/lib/brainiac/restart.rb +94 -0
- data/lib/brainiac/routes/api.rb +427 -0
- data/lib/brainiac/version.rb +1 -1
- data/lib/brainiac/zoho_mail_api.rb +2 -1
- data/lib/brainiac.rb +7 -0
- data/monitor/daemon.rb +4 -27
- data/monitor/shared.rb +247 -0
- data/monitor/{waybar-deploy-env.rb → waybar/deploy_env.rb} +14 -86
- data/monitor/waybar/setup.rb +232 -0
- data/monitor/waybar/status.rb +51 -0
- data/monitor/{view-logs-rofi.rb → waybar/view_logs.rb} +21 -88
- data/monitor/{deploy-env-macos.rb → xbar/deploy_env.rb} +3 -2
- data/monitor/xbar/plugin.rb +155 -0
- data/monitor/{setup-menubar.rb → xbar/setup.rb} +14 -30
- data/receiver.rb +91 -450
- data/templates/brainiac.json.example +9 -0
- data/templates/cli-providers/kiro.json.example +8 -2
- data/templates/plugins.json.example +3 -0
- metadata +30 -20
- data/lib/user_registry.rb +0 -159
- data/monitor/menubar.rb +0 -295
- data/monitor/setup-waybar-deploy-envs.rb +0 -121
- data/monitor/setup-waybar-deployments.rb +0 -96
- data/monitor/setup-waybar-module.rb +0 -113
- data/monitor/setup-xbar-plugin.rb +0 -35
- data/monitor/view-logs.rb +0 -119
- data/monitor/waybar-config-updater.rb +0 -56
- data/monitor/waybar-deployments.rb +0 -239
- data/monitor/waybar.rb +0 -146
- data/monitor/xbar.3s.rb +0 -179
- /data/lib/brainiac/{card_index.rb → handlers/fizzy/card_index.rb} +0 -0
- /data/monitor/{open-action.sh → xbar/open_action.sh} +0 -0
- /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
|
-
|
|
85
|
-
|
|
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
|