brainiac-fizzy 0.0.25 → 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 +4 -4
- data/lib/brainiac/plugins/fizzy/cli.rb +67 -2
- data/lib/brainiac/plugins/fizzy/delegators.rb +12 -0
- data/lib/brainiac/plugins/fizzy/handlers/assignment.rb +137 -0
- data/lib/brainiac/plugins/fizzy/handlers/comments.rb +6 -0
- data/lib/brainiac/plugins/fizzy/helpers.rb +31 -0
- data/lib/brainiac/plugins/fizzy/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b9d6e5b3e6e6ea9306dc6fd9ce800dffc01e46f99b05f2861e0acf096793fe5
|
|
4
|
+
data.tar.gz: ec15a0b6f7a498de7662b8bd662fb519ae45f8a14d66e5912cf6dd540cc3726b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51284c7c4cca09f1bde28d3589fc7c17b29314e898f4de8bd7345fa1fd930baf6501ef47e42a241b40f17baede635780676154e7c6f456a70ea6f1d9902da59b
|
|
7
|
+
data.tar.gz: 868fc4f7e922d67138125385c486f87bc221766ddadf45e640ad880ee57914f04a772bdd3b688087c505a66cfa59f7643e1134f5731eb0ef67f3e42b6f80a700
|
|
@@ -658,8 +658,73 @@ module Brainiac
|
|
|
658
658
|
end
|
|
659
659
|
|
|
660
660
|
# Subcommand names for bash completion.
|
|
661
|
-
|
|
662
|
-
|
|
661
|
+
# When called with no args, returns top-level subcommands.
|
|
662
|
+
# When called with args (the words typed so far after the plugin name),
|
|
663
|
+
# returns context-sensitive completions for nested subcommands.
|
|
664
|
+
def self.completions(args = [])
|
|
665
|
+
return %w[board config setup status] if args.empty?
|
|
666
|
+
|
|
667
|
+
case args[0]
|
|
668
|
+
when "board"
|
|
669
|
+
board_completions(args[1..])
|
|
670
|
+
else
|
|
671
|
+
[]
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# Nested completions for `brainiac fizzy board ...`
|
|
676
|
+
def self.board_completions(args)
|
|
677
|
+
board_subcommands = %w[assign columns list ls setup webhook]
|
|
678
|
+
return board_subcommands if args.empty?
|
|
679
|
+
|
|
680
|
+
case args[0]
|
|
681
|
+
when "assign"
|
|
682
|
+
board_assign_completions(args[1..])
|
|
683
|
+
when "columns", "webhook"
|
|
684
|
+
board_key_completions(args[1..])
|
|
685
|
+
else
|
|
686
|
+
[]
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
# Completions for `brainiac fizzy board assign <project> <board>`
|
|
691
|
+
def self.board_assign_completions(args)
|
|
692
|
+
case args.length
|
|
693
|
+
when 0
|
|
694
|
+
load_project_keys
|
|
695
|
+
when 1
|
|
696
|
+
load_board_keys
|
|
697
|
+
else
|
|
698
|
+
[]
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Completions for commands that take a board key as next arg
|
|
703
|
+
def self.board_key_completions(args)
|
|
704
|
+
return load_board_keys if args.empty?
|
|
705
|
+
|
|
706
|
+
[]
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
# Load board keys from fizzy.json for completion
|
|
710
|
+
def self.load_board_keys
|
|
711
|
+
config_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "fizzy.json")
|
|
712
|
+
return [] unless File.exist?(config_file)
|
|
713
|
+
|
|
714
|
+
config = JSON.parse(File.read(config_file))
|
|
715
|
+
(config["boards"] || {}).keys
|
|
716
|
+
rescue StandardError
|
|
717
|
+
[]
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
# Load project keys from projects.json for completion
|
|
721
|
+
def self.load_project_keys
|
|
722
|
+
projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
|
|
723
|
+
return [] unless File.exist?(projects_file)
|
|
724
|
+
|
|
725
|
+
JSON.parse(File.read(projects_file)).keys
|
|
726
|
+
rescue StandardError
|
|
727
|
+
[]
|
|
663
728
|
end
|
|
664
729
|
|
|
665
730
|
# Called by brainiac CLI after `agent create` — prompts for Fizzy user ID.
|
|
@@ -25,6 +25,18 @@ def prefetch_card_context(card_number, repo_path:, agent_name: nil)
|
|
|
25
25
|
Brainiac::Plugins::Fizzy::Helpers.prefetch_card_context(card_number, repo_path: repo_path, agent_name: agent_name)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def tag_names(tags)
|
|
29
|
+
Brainiac::Plugins::Fizzy::Helpers.tag_names(tags)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def card_has_tag?(tags, name)
|
|
33
|
+
Brainiac::Plugins::Fizzy::Helpers.card_has_tag?(tags, name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fetch_card_tags(card_number, repo_path:, env: nil)
|
|
37
|
+
Brainiac::Plugins::Fizzy::Helpers.fetch_card_tags(card_number, repo_path: repo_path, env: env)
|
|
38
|
+
end
|
|
39
|
+
|
|
28
40
|
def resolve_github_agent_env(agent_name, github_repo)
|
|
29
41
|
Brainiac::Plugins::Fizzy::Helpers.resolve_github_agent_env(agent_name, github_repo)
|
|
30
42
|
end
|
|
@@ -58,6 +58,10 @@ def handle_card_assigned(payload, board_key: nil)
|
|
|
58
58
|
inline_effort: initial_effort
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
+
maybe_create_ephemeral_belt_env(
|
|
62
|
+
worktree_path: worktree_path, card_number: card_number, project_key: project_key, tags: tags
|
|
63
|
+
)
|
|
64
|
+
|
|
61
65
|
dispatch_assigned_card(
|
|
62
66
|
card_number: card_number, card_internal_id: card_internal_id, title: title, tags: tags,
|
|
63
67
|
branch: branch, worktree_path: worktree_path, project_config: project_config, project_key: project_key,
|
|
@@ -183,3 +187,136 @@ def dispatch_assigned_card(card_number:, card_internal_id:, title:, tags:, branc
|
|
|
183
187
|
|
|
184
188
|
[200, { status: "processed", card: card_number, branch: branch, project: project_key, agent: agent_name }.to_json]
|
|
185
189
|
end
|
|
190
|
+
|
|
191
|
+
# Ensure an ephemeral Belt environment is configured in the worktree when the
|
|
192
|
+
# card has a `deploy` tag. Fizzy has no tag-added webhook, so comment handlers
|
|
193
|
+
# pass fetch_live_tags: true to read current tags from `fizzy card show`.
|
|
194
|
+
#
|
|
195
|
+
# `belt g environment` is synchronous so the worktree is configured before the
|
|
196
|
+
# agent starts. The actual `belt deploy` runs in the background.
|
|
197
|
+
def maybe_create_ephemeral_belt_env(worktree_path:, card_number:, project_key:, tags: nil,
|
|
198
|
+
repo_path: nil, agent_name: nil, fetch_live_tags: false)
|
|
199
|
+
unless defined?(BeltConfig) && defined?(BeltEnvironment)
|
|
200
|
+
LOG.debug "[EphemeralEnv] Belt utilities not available — skipping"
|
|
201
|
+
return
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
unless worktree_path && File.directory?(worktree_path)
|
|
205
|
+
LOG.debug "[EphemeralEnv] Worktree missing at #{worktree_path.inspect} — skipping"
|
|
206
|
+
return
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
unless belt_app_for_ephemeral?(worktree_path)
|
|
210
|
+
LOG.debug "[EphemeralEnv] #{project_key} is not a Belt app — skipping"
|
|
211
|
+
return
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
unless BeltConfig.ephemeral_deploys_enabled?
|
|
215
|
+
LOG.info "[EphemeralEnv] Ephemeral deploys disabled in basecamp.json"
|
|
216
|
+
return
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
resolved_tags = resolve_ephemeral_env_tags(
|
|
220
|
+
tags, card_number: card_number, repo_path: repo_path || worktree_path,
|
|
221
|
+
agent_name: agent_name, fetch_live_tags: fetch_live_tags
|
|
222
|
+
)
|
|
223
|
+
tag_list = tag_names(resolved_tags)
|
|
224
|
+
unless tag_list.include?("deploy")
|
|
225
|
+
LOG.debug "[EphemeralEnv] Card ##{card_number} tags=#{tag_list.inspect} — no deploy tag, skipping"
|
|
226
|
+
return
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
parent_env = BeltConfig.parent_env_for(project_key)
|
|
230
|
+
unless parent_env
|
|
231
|
+
LOG.info "[EphemeralEnv] Card ##{card_number} has deploy tag but no parent env configured for #{project_key}"
|
|
232
|
+
return
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
env_name = BeltConfig.ephemeral_env_for_card(card_number)
|
|
236
|
+
env_dir = File.join(worktree_path, "infrastructure", env_name.to_s)
|
|
237
|
+
|
|
238
|
+
if File.directory?(env_dir)
|
|
239
|
+
LOG.info "[EphemeralEnv] Card ##{card_number} has deploy tag; #{env_name} already configured at #{env_dir}"
|
|
240
|
+
track_ephemeral_env_if_needed(env_name, card_number, project_key, parent_env, worktree_path)
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
parent_dir = File.join(worktree_path, "infrastructure", parent_env.to_s)
|
|
245
|
+
unless File.directory?(parent_dir)
|
|
246
|
+
LOG.error "[EphemeralEnv] Parent env '#{parent_env}' not found at #{parent_dir}"
|
|
247
|
+
return
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
LOG.info "[EphemeralEnv] Card ##{card_number} has deploy tag; #{env_name} not in worktree — creating from '#{parent_env}'"
|
|
251
|
+
create_and_deploy_ephemeral_env(
|
|
252
|
+
worktree_path: worktree_path, env_name: env_name, parent_env: parent_env,
|
|
253
|
+
card_number: card_number, project_key: project_key
|
|
254
|
+
)
|
|
255
|
+
rescue StandardError => e
|
|
256
|
+
LOG.error "[EphemeralEnv] Error creating ephemeral env: #{e.message}"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def ensure_ephemeral_env_for_comment(ctx, card_number, worktree)
|
|
260
|
+
return unless card_number && worktree && File.directory?(worktree)
|
|
261
|
+
|
|
262
|
+
maybe_create_ephemeral_belt_env(
|
|
263
|
+
worktree_path: worktree, card_number: card_number, project_key: ctx.project_key,
|
|
264
|
+
tags: ctx.card_tags, repo_path: ctx.project_config["repo_path"],
|
|
265
|
+
agent_name: ctx.agent_name, fetch_live_tags: true
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def belt_app_for_ephemeral?(worktree_path)
|
|
270
|
+
return BeltEnvironment.belt_app?(worktree_path) if defined?(BeltEnvironment) && BeltEnvironment.respond_to?(:belt_app?)
|
|
271
|
+
|
|
272
|
+
%w[config/routes.rb config/routes.tf.rb infrastructure/routes.tf.rb].any? do |rel|
|
|
273
|
+
File.exist?(File.join(worktree_path, rel))
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def resolve_ephemeral_env_tags(tags, card_number:, repo_path:, agent_name:, fetch_live_tags:)
|
|
278
|
+
return tags unless fetch_live_tags && card_number
|
|
279
|
+
|
|
280
|
+
live = fetch_card_tags(card_number, repo_path: repo_path, env: fizzy_env_for(agent_name || AI_AGENT_NAME))
|
|
281
|
+
if live
|
|
282
|
+
LOG.info "[EphemeralEnv] Card ##{card_number} live tags: #{tag_names(live).inspect}"
|
|
283
|
+
live
|
|
284
|
+
else
|
|
285
|
+
LOG.warn "[EphemeralEnv] Could not fetch live tags for card ##{card_number} — falling back to webhook tags #{tag_names(tags).inspect}"
|
|
286
|
+
tags
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def track_ephemeral_env_if_needed(env_name, card_number, project_key, parent_env, worktree_path)
|
|
291
|
+
return if BeltConfig.ephemeral_env?(env_name)
|
|
292
|
+
|
|
293
|
+
BeltConfig.track_ephemeral_env(env_name,
|
|
294
|
+
"card_number" => card_number,
|
|
295
|
+
"project" => project_key,
|
|
296
|
+
"parent_env" => parent_env,
|
|
297
|
+
"worktree" => worktree_path)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def create_and_deploy_ephemeral_env(worktree_path:, env_name:, parent_env:, card_number:, project_key:)
|
|
301
|
+
success = BeltEnvironment.create_environment(
|
|
302
|
+
worktree: worktree_path, env_name: env_name, parent_env: parent_env
|
|
303
|
+
)
|
|
304
|
+
unless success
|
|
305
|
+
LOG.error "[EphemeralEnv] Failed to create environment '#{env_name}'"
|
|
306
|
+
return
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
BeltConfig.track_ephemeral_env(env_name,
|
|
310
|
+
"card_number" => card_number,
|
|
311
|
+
"project" => project_key,
|
|
312
|
+
"parent_env" => parent_env,
|
|
313
|
+
"worktree" => worktree_path)
|
|
314
|
+
LOG.info "[EphemeralEnv] Configured #{env_name} in worktree, deploying in background"
|
|
315
|
+
|
|
316
|
+
Thread.new do
|
|
317
|
+
frontend_only = BeltEnvironment.frontend_only_changes?(worktree: worktree_path)
|
|
318
|
+
BeltEnvironment.deploy(worktree: worktree_path, env_name: env_name, frontend_only: frontend_only)
|
|
319
|
+
rescue StandardError => e
|
|
320
|
+
LOG.error "[EphemeralEnv] Error deploying '#{env_name}': #{e.message}"
|
|
321
|
+
end
|
|
322
|
+
end
|
|
@@ -453,6 +453,10 @@ def handle_existing_card_comment(ctx)
|
|
|
453
453
|
work_dir = worktree && File.directory?(worktree) ? worktree : ctx.project_config["repo_path"]
|
|
454
454
|
card_key = "card-#{card_number || ctx.card_internal_id}"
|
|
455
455
|
|
|
456
|
+
# Fizzy has no tag-added webhook. On comment, fetch live tags and configure
|
|
457
|
+
# the Belt env in this worktree before dispatching (or queueing) the agent.
|
|
458
|
+
ensure_ephemeral_env_for_comment(ctx, card_number, worktree)
|
|
459
|
+
|
|
456
460
|
# Session management (wait, supersede, or queue)
|
|
457
461
|
queued = handle_session_conflict(ctx, card_key, card_number, work_dir)
|
|
458
462
|
return queued if queued
|
|
@@ -491,6 +495,8 @@ def handle_new_mention(ctx)
|
|
|
491
495
|
react_to_comment(card_number, ctx.comment_id, ctx.project_config, ctx.agent_name, "👀")
|
|
492
496
|
|
|
493
497
|
worktree_path, branch = setup_new_mention_worktree(ctx, card_number, card_title)
|
|
498
|
+
ensure_ephemeral_env_for_comment(ctx, card_number, worktree_path)
|
|
499
|
+
|
|
494
500
|
dispatch_new_mention(ctx, card_key: card_key, card_number: card_number,
|
|
495
501
|
card_title: card_title, branch: branch, worktree_path: worktree_path)
|
|
496
502
|
end
|
|
@@ -60,6 +60,37 @@ module Brainiac
|
|
|
60
60
|
context
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
# Normalize Fizzy tags from webhook hashes ({ "name" => "deploy" })
|
|
64
|
+
# or API strings ("deploy") into a lowercase name list.
|
|
65
|
+
def tag_names(tags)
|
|
66
|
+
Array(tags).filter_map do |tag|
|
|
67
|
+
name = tag.is_a?(Hash) ? (tag["name"] || tag[:name]) : tag
|
|
68
|
+
normalized = name.to_s.downcase
|
|
69
|
+
normalized unless normalized.empty?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def card_has_tag?(tags, name)
|
|
74
|
+
tag_names(tags).include?(name.to_s.downcase)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Live tags from `fizzy card show`. Returns nil on failure so callers
|
|
78
|
+
# can fall back to webhook payload tags. Fizzy has no tag-added webhook;
|
|
79
|
+
# comment payloads may omit or stale-cache tags, so this is the source of truth.
|
|
80
|
+
def fetch_card_tags(card_number, repo_path:, env: nil)
|
|
81
|
+
return nil unless card_number && repo_path
|
|
82
|
+
|
|
83
|
+
env ||= default_fizzy_env
|
|
84
|
+
output = run_cmd("fizzy", "card", "show", card_number.to_s, chdir: repo_path, env: env)
|
|
85
|
+
card = JSON.parse(output)["data"]
|
|
86
|
+
return nil unless card
|
|
87
|
+
|
|
88
|
+
Array(card["tags"])
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
LOG.warn "[Fizzy] Could not fetch tags for card ##{card_number}: #{e.message}" if defined?(LOG)
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
63
94
|
def fetch_card_details(card_number, repo_path:, env:)
|
|
64
95
|
output = run_cmd("fizzy", "card", "show", card_number.to_s, chdir: repo_path, env: env)
|
|
65
96
|
card = JSON.parse(output)["data"]
|