brainiac-fizzy 0.0.16 → 0.0.18

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: 6ed1ef3ae4c3db228a9a98a796579e7450e25fad4e19fa5ce075777e677342cc
4
- data.tar.gz: 9e5ceac2510d64194bc498850871ca8105aeea24b742e9930ede13c761928f50
3
+ metadata.gz: 34a03eed47ca8ae2a7de8ec8351962c5a3aa59652d7466c14221d4919866519f
4
+ data.tar.gz: e18090ad45a15ce328c6c1fdabc267e32c29868bd28e36f6540af7ed1ace6436
5
5
  SHA512:
6
- metadata.gz: 73f14f9e28f8eaba2558fac32e14116d0255fdb513cd2ba13753f1e5babe9a311807620832b75cd4b46282473e0603a1127a38483f18034177c0d983a84b5146
7
- data.tar.gz: d45acdf7a664c1585b262230a0b1cf70eff969699fc80c270193ccedf0d902ed7876fc6d3eb353acf4768217bd88babced1f10db835b6aa7e13edde62b57b27c
6
+ metadata.gz: 4db395ff509a514e144c4237affb376ac43de3d2a04ed0a5e89a12f5260c7b02ad144344b3bab5e471f9a617df1af3bbcc99ed29c2151c2e3b61d99df6f5f9c4
7
+ data.tar.gz: d9c0cfdcec762022846ed40cc2e8713504f55c6206df41024c484ec9eade1378d60d82e3dd593ebdf76261d7cbcb8bde25f10dfb863830af2e76aca431b960c2
@@ -12,6 +12,8 @@ module Brainiac
12
12
  BRAINIAC_DIR = ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac"))
13
13
  FIZZY_CONFIG_FILE = File.join(BRAINIAC_DIR, "fizzy.json")
14
14
 
15
+ PROJECTS_FILE = File.join(BRAINIAC_DIR, "projects.json")
16
+
15
17
  class << self
16
18
  def run(args)
17
19
  command = args.shift
@@ -23,6 +25,8 @@ module Brainiac
23
25
  cmd_status
24
26
  when "setup"
25
27
  cmd_setup
28
+ when "board"
29
+ cmd_board(args)
26
30
  else
27
31
  print_help
28
32
  end
@@ -97,6 +101,463 @@ module Brainiac
97
101
  puts "Webhook URL: https://<your-ngrok>.ngrok-free.app/fizzy/<board-key>"
98
102
  end
99
103
 
104
+ def cmd_board(args)
105
+ sub = args.shift
106
+ case sub
107
+ when "setup", "add"
108
+ board_setup
109
+ when "list", "ls"
110
+ board_list
111
+ when "assign"
112
+ board_assign(args)
113
+ when "columns", "refresh"
114
+ board_columns(args)
115
+ when "webhook"
116
+ board_webhook(args)
117
+ else
118
+ puts "Usage: brainiac fizzy board <setup|list|assign|columns|webhook>"
119
+ puts ""
120
+ puts "Commands:"
121
+ puts " setup Add a new board (interactive, fetches columns from Fizzy)"
122
+ puts " list List configured boards and their columns"
123
+ puts " assign <project> <board_key> Assign a project to a board"
124
+ puts " columns <board_key> Refresh columns for a board from Fizzy"
125
+ puts " webhook <board_key> [url] Create/replace webhook and save the signing secret"
126
+ end
127
+ end
128
+
129
+ # --- board setup (split into phases) ---
130
+
131
+ def board_setup
132
+ puts "Fizzy Board Setup"
133
+ puts "================="
134
+ puts ""
135
+
136
+ board = board_setup_select_board
137
+ return unless board
138
+
139
+ board_key = board_setup_choose_key(board["name"])
140
+ column_map = board_setup_map_columns(board["id"], board["name"])
141
+ secret = board_setup_ask_secret(board_key)
142
+
143
+ config = board_setup_save(board_key, board["id"], column_map, secret)
144
+
145
+ puts ""
146
+ puts "✓ Board \"#{board["name"]}\" added as '#{board_key}' in #{FIZZY_CONFIG_FILE}"
147
+ puts " Columns: #{column_map.keys.join(", ")}" unless column_map.empty?
148
+
149
+ board_setup_offer_webhook(config, board_key, board["id"])
150
+
151
+ puts ""
152
+ puts "Next steps:"
153
+ puts " brainiac fizzy board assign <project-key> #{board_key}"
154
+ end
155
+
156
+ def board_setup_select_board
157
+ boards_json = run_fizzy("board", "list", "--all", "--json")
158
+ unless boards_json
159
+ puts "Error: Could not fetch boards from Fizzy CLI."
160
+ puts " Ensure 'fizzy' is on your PATH and authenticated."
161
+ puts " Run: fizzy doctor"
162
+ return nil
163
+ end
164
+
165
+ boards = JSON.parse(boards_json)["data"] || []
166
+ if boards.empty?
167
+ puts "No boards found in your Fizzy account."
168
+ return nil
169
+ end
170
+
171
+ puts "Available boards:"
172
+ boards.each_with_index do |board, i|
173
+ puts " #{i + 1}) #{board["name"]} (#{board["id"]})"
174
+ end
175
+ puts ""
176
+ print "Select board number: "
177
+ choice = $stdin.gets&.chomp&.to_i
178
+ return puts("Cancelled.") unless choice&.positive? && choice <= boards.size
179
+
180
+ boards[choice - 1]
181
+ end
182
+
183
+ def board_setup_choose_key(board_name)
184
+ suggested_key = board_name.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/^_|_$/, "")
185
+ print "Board key for config [#{suggested_key}]: "
186
+ board_key = $stdin.gets&.chomp
187
+ board_key = suggested_key if board_key.nil? || board_key.empty?
188
+ board_key
189
+ end
190
+
191
+ def board_setup_map_columns(board_id, board_name)
192
+ columns_json = run_fizzy("column", "list", "--board", board_id, "--json")
193
+ columns = columns_json ? (JSON.parse(columns_json)["data"] || []) : []
194
+ custom_columns = columns.reject { |c| c["pseudo"] }
195
+
196
+ puts ""
197
+ puts "Columns for \"#{board_name}\":"
198
+ if columns.empty?
199
+ puts " (no columns found)"
200
+ else
201
+ columns.each do |col|
202
+ pseudo_tag = col["pseudo"] ? " [built-in]" : ""
203
+ puts " • #{col["name"]} (#{col["id"]})#{pseudo_tag}"
204
+ end
205
+ end
206
+
207
+ puts ""
208
+ puts "Which columns should brainiac track? Enter config names for each."
209
+ puts "(These map to column transitions like right_now, needs_review, uat, etc.)"
210
+ puts "Leave blank to skip a column."
211
+ puts ""
212
+
213
+ prompt_column_mapping(custom_columns)
214
+ end
215
+
216
+ def board_setup_ask_secret(board_key)
217
+ puts ""
218
+ puts "Webhook setup:"
219
+ puts " Fizzy generates the signing secret when you create a webhook."
220
+ print "Paste the webhook signing_secret from Fizzy (or 'skip' to set later): "
221
+ secret = $stdin.gets&.chomp
222
+ if secret.nil? || secret.empty? || secret == "skip"
223
+ puts " ⚠ No secret set — you'll need to add it to fizzy.json manually later."
224
+ puts " Create the webhook in Fizzy, then paste the signing_secret into:"
225
+ puts " #{FIZZY_CONFIG_FILE} → boards.#{board_key}.webhook_secret"
226
+ nil
227
+ else
228
+ secret
229
+ end
230
+ end
231
+
232
+ def board_setup_save(board_key, board_id, column_map, secret)
233
+ config = load_fizzy_config
234
+ config["boards"] ||= {}
235
+ board_entry = { "board_id" => board_id, "columns" => column_map }
236
+ board_entry["webhook_secret"] = secret if secret
237
+ config["boards"][board_key] = board_entry
238
+ save_fizzy_config(config)
239
+ config
240
+ end
241
+
242
+ def board_setup_offer_webhook(config, board_key, board_id)
243
+ puts ""
244
+ print "Create webhook in Fizzy now? [Y/n]: "
245
+ answer = $stdin.gets&.chomp&.downcase
246
+ return if answer == "n"
247
+
248
+ webhook_url = prompt_webhook_url(board_key)
249
+ return unless webhook_url && !webhook_url.empty?
250
+
251
+ signing_secret = create_fizzy_webhook(board_id, board_key, webhook_url)
252
+ return unless signing_secret
253
+
254
+ config["boards"][board_key]["webhook_secret"] = signing_secret
255
+ save_fizzy_config(config)
256
+ puts "✓ Webhook created! Signing secret saved to fizzy.json."
257
+ end
258
+
259
+ # --- board list ---
260
+
261
+ def board_list
262
+ config = load_fizzy_config
263
+ boards = config["boards"] || {}
264
+
265
+ if boards.empty?
266
+ puts "No boards configured."
267
+ puts "Run 'brainiac fizzy board setup' to add one."
268
+ return
269
+ end
270
+
271
+ projects = load_projects_config
272
+
273
+ puts "Configured boards:"
274
+ boards.each do |key, board_config|
275
+ puts ""
276
+ puts " #{key}:"
277
+ puts " Board ID: #{board_config["board_id"]}"
278
+ puts " Webhook: #{board_config["webhook_secret"] ? "configured" : "not set"}"
279
+
280
+ columns = board_config["columns"] || {}
281
+ if columns.any?
282
+ puts " Columns:"
283
+ columns.each { |name, id| puts " #{name}: #{id}" }
284
+ else
285
+ puts " Columns: (none configured)"
286
+ end
287
+
288
+ assigned = projects.select { |_, cfg| cfg["fizzy_board"] == key }.keys
289
+ puts " Projects: #{assigned.any? ? assigned.join(", ") : "(none)"}"
290
+ end
291
+ end
292
+
293
+ # --- board assign ---
294
+
295
+ def board_assign(args)
296
+ project_key = args.shift
297
+ board_key = args.shift
298
+
299
+ unless project_key && board_key
300
+ puts "Usage: brainiac fizzy board assign <project-key> <board-key>"
301
+ puts ""
302
+ puts "Available boards:"
303
+ config = load_fizzy_config
304
+ (config["boards"] || {}).each_key { |k| puts " #{k}" }
305
+ puts ""
306
+ puts "Available projects:"
307
+ load_projects_config.each_key { |k| puts " #{k}" }
308
+ return
309
+ end
310
+
311
+ config = load_fizzy_config
312
+ unless config.dig("boards", board_key)
313
+ puts "Error: Board '#{board_key}' not found in fizzy.json."
314
+ puts "Available: #{(config["boards"] || {}).keys.join(", ")}"
315
+ return
316
+ end
317
+
318
+ projects = load_projects_config
319
+ unless projects.key?(project_key)
320
+ puts "Error: Project '#{project_key}' not found in projects.json."
321
+ puts "Available: #{projects.keys.join(", ")}"
322
+ return
323
+ end
324
+
325
+ projects[project_key]["fizzy_board"] = board_key
326
+ save_projects_config(projects)
327
+
328
+ puts "✓ Assigned project '#{project_key}' to board '#{board_key}'"
329
+ end
330
+
331
+ # --- board columns ---
332
+
333
+ def board_columns(args)
334
+ board_key = args.shift
335
+ unless board_key
336
+ puts "Usage: brainiac fizzy board columns <board-key>"
337
+ puts ""
338
+ puts "Fetches current columns from Fizzy and lets you update the config."
339
+ return
340
+ end
341
+
342
+ config = load_fizzy_config
343
+ board_config = config.dig("boards", board_key)
344
+ unless board_config
345
+ puts "Error: Board '#{board_key}' not found in fizzy.json."
346
+ puts "Available: #{(config["boards"] || {}).keys.join(", ")}"
347
+ return
348
+ end
349
+
350
+ board_id = board_config["board_id"]
351
+ columns_json = run_fizzy("column", "list", "--board", board_id, "--json")
352
+ unless columns_json
353
+ puts "Error: Could not fetch columns from Fizzy."
354
+ return
355
+ end
356
+
357
+ columns = JSON.parse(columns_json)["data"] || []
358
+ custom_columns = columns.reject { |c| c["pseudo"] }
359
+ existing_map = board_config["columns"] || {}
360
+
361
+ puts "Columns for board '#{board_key}' (#{board_id}):"
362
+ puts ""
363
+
364
+ columns.each do |col|
365
+ pseudo_tag = col["pseudo"] ? " [built-in]" : ""
366
+ existing_key = existing_map.key(col["id"])
367
+ mapped = existing_key ? " → #{existing_key}" : ""
368
+ puts " • #{col["name"]} (#{col["id"]})#{pseudo_tag}#{mapped}"
369
+ end
370
+
371
+ puts ""
372
+ puts "Update column mappings? (Enter new config keys, blank to keep, '-' to remove)"
373
+ puts ""
374
+
375
+ new_map = prompt_column_mapping(custom_columns, existing_map)
376
+
377
+ config["boards"][board_key]["columns"] = new_map
378
+ save_fizzy_config(config)
379
+
380
+ puts ""
381
+ puts "✓ Updated columns for '#{board_key}': #{new_map.keys.join(", ")}"
382
+ end
383
+
384
+ # --- board webhook (split into phases) ---
385
+
386
+ def board_webhook(args)
387
+ board_key = args.shift
388
+ webhook_url = args.shift
389
+
390
+ unless board_key
391
+ puts "Usage: brainiac fizzy board webhook <board-key> [payload-url]"
392
+ puts ""
393
+ puts "Creates a webhook in Fizzy for the board and saves the signing secret."
394
+ puts "If the board already has a webhook, deletes it first."
395
+ puts ""
396
+ puts "Available boards:"
397
+ config = load_fizzy_config
398
+ (config["boards"] || {}).each_key { |k| puts " #{k}" }
399
+ return
400
+ end
401
+
402
+ config = load_fizzy_config
403
+ board_config = config.dig("boards", board_key)
404
+ unless board_config
405
+ puts "Error: Board '#{board_key}' not found in fizzy.json."
406
+ return
407
+ end
408
+
409
+ board_id = board_config["board_id"]
410
+
411
+ return if remove_existing_webhooks(board_id) == :cancelled
412
+
413
+ webhook_url ||= prompt_webhook_url(board_key)
414
+ if webhook_url.nil? || webhook_url.empty?
415
+ puts "Error: Payload URL is required."
416
+ return
417
+ end
418
+
419
+ board_webhook_create_and_save(config, board_key, board_id, webhook_url)
420
+ end
421
+
422
+ def remove_existing_webhooks(board_id)
423
+ existing_json = run_fizzy("webhook", "list", "--board", board_id, "--all", "--json")
424
+ return :proceed unless existing_json
425
+
426
+ existing = JSON.parse(existing_json)["data"] || []
427
+ brainiac_webhooks = existing.select { |w| w["name"]&.start_with?("brainiac") }
428
+ return :proceed unless brainiac_webhooks.any?
429
+
430
+ puts "Existing brainiac webhook(s) found:"
431
+ brainiac_webhooks.each do |w|
432
+ status = w["active"] ? "active" : "inactive"
433
+ puts " • #{w["name"]} (#{status}) → #{w["payload_url"]}"
434
+ end
435
+ print "Delete existing and create new? [Y/n]: "
436
+ answer = $stdin.gets&.chomp&.downcase
437
+ if answer == "n"
438
+ puts "Cancelled."
439
+ return :cancelled
440
+ end
441
+ brainiac_webhooks.each do |w|
442
+ run_fizzy("webhook", "delete", w["id"], "--board", board_id)
443
+ puts " Deleted: #{w["name"]}"
444
+ end
445
+ :proceed
446
+ end
447
+
448
+ def board_webhook_create_and_save(config, board_key, board_id, webhook_url)
449
+ signing_secret = create_fizzy_webhook(board_id, board_key, webhook_url)
450
+ webhook_id = @last_webhook_id
451
+
452
+ if signing_secret
453
+ config["boards"][board_key]["webhook_secret"] = signing_secret
454
+ save_fizzy_config(config)
455
+ puts "✓ Webhook created (ID: #{webhook_id})"
456
+ puts " URL: #{webhook_url}"
457
+ puts " Signing secret saved to fizzy.json"
458
+ else
459
+ puts "✓ Webhook created but no signing_secret in response."
460
+ puts " Check: fizzy webhook show #{webhook_id} --board #{board_id}"
461
+ end
462
+ end
463
+
464
+ # --- shared helpers ---
465
+
466
+ def prompt_column_mapping(columns, existing_map = {})
467
+ column_map = {}
468
+ columns.each do |col|
469
+ existing_key = existing_map.key(col["id"])
470
+ suggested = existing_key || col["name"].downcase.gsub(/[^a-z0-9]+/, "_").gsub(/^_|_$/, "")
471
+ print " \"#{col["name"]}\" → config key [#{suggested}]: "
472
+ key = $stdin.gets&.chomp
473
+ key = suggested if key.nil? || key.empty?
474
+ next if ["-", "skip"].include?(key)
475
+
476
+ column_map[key] = col["id"]
477
+ end
478
+ column_map
479
+ end
480
+
481
+ def prompt_webhook_url(board_key)
482
+ detected_url = detect_ngrok_url
483
+ if detected_url
484
+ default_url = "#{detected_url}/fizzy/#{board_key}"
485
+ print "Webhook payload URL [#{default_url}]: "
486
+ input = $stdin.gets&.chomp
487
+ input.nil? || input.empty? ? default_url : input
488
+ else
489
+ print "Webhook payload URL (e.g., https://your-ngrok.app/fizzy/#{board_key}): "
490
+ $stdin.gets&.chomp
491
+ end
492
+ end
493
+
494
+ def create_fizzy_webhook(board_id, board_key, webhook_url)
495
+ actions = "card_assigned,card_closed,card_published,card_reopened,card_triaged,comment_created"
496
+ webhook_json = run_fizzy("webhook", "create", "--board", board_id,
497
+ "--name", "brainiac-#{board_key}",
498
+ "--url", webhook_url,
499
+ "--actions", actions, "--json")
500
+ unless webhook_json
501
+ puts "⚠ Failed to create webhook. Create it manually in Fizzy."
502
+ return nil
503
+ end
504
+
505
+ webhook_data = JSON.parse(webhook_json)["data"]
506
+ @last_webhook_id = webhook_data&.dig("id")
507
+ webhook_data&.dig("signing_secret")
508
+ end
509
+
510
+ def load_fizzy_config
511
+ if File.exist?(FIZZY_CONFIG_FILE)
512
+ JSON.parse(File.read(FIZZY_CONFIG_FILE))
513
+ else
514
+ { "boards" => {}, "authorized_users" => [] }
515
+ end
516
+ rescue JSON::ParserError
517
+ { "boards" => {}, "authorized_users" => [] }
518
+ end
519
+
520
+ def save_fizzy_config(config)
521
+ File.write(FIZZY_CONFIG_FILE, JSON.pretty_generate(config))
522
+ end
523
+
524
+ def load_projects_config
525
+ if File.exist?(PROJECTS_FILE)
526
+ JSON.parse(File.read(PROJECTS_FILE))
527
+ else
528
+ {}
529
+ end
530
+ rescue JSON::ParserError
531
+ {}
532
+ end
533
+
534
+ def save_projects_config(projects)
535
+ File.write(PROJECTS_FILE, JSON.pretty_generate(projects))
536
+ end
537
+
538
+ def run_fizzy(*)
539
+ require "open3"
540
+ output, status = Open3.capture2("fizzy", *)
541
+ status.success? ? output : nil
542
+ rescue Errno::ENOENT
543
+ nil
544
+ end
545
+
546
+ def detect_ngrok_url
547
+ require "net/http"
548
+ uri = URI("http://127.0.0.1:4040/api/tunnels")
549
+ http = Net::HTTP.new(uri.host, uri.port)
550
+ http.open_timeout = 2
551
+ http.read_timeout = 2
552
+ response = http.get(uri.path)
553
+ data = JSON.parse(response.body)
554
+ tunnels = data["tunnels"] || []
555
+ https_tunnel = tunnels.find { |t| t["public_url"]&.start_with?("https://") }
556
+ https_tunnel&.dig("public_url")
557
+ rescue StandardError
558
+ nil
559
+ end
560
+
100
561
  def print_help
101
562
  puts <<~HELP
102
563
  Usage: brainiac fizzy <command>
@@ -105,6 +566,10 @@ module Brainiac
105
566
  config Show Fizzy config
106
567
  status Check Fizzy status via server API
107
568
  setup Show setup guide
569
+ board setup Add a new board (interactive, fetches columns from Fizzy)
570
+ board list List configured boards and their columns
571
+ board assign <project> <board_key> Assign a project to a board
572
+ board columns <board_key> Refresh columns for a board from Fizzy
108
573
 
109
574
  Fizzy handles card assignment, comments, @mentions, cross-agent reviews,
110
575
  duplicate detection, and planning mode via webhooks.
@@ -155,7 +620,6 @@ module Brainiac
155
620
  config = JSON.parse(File.read(config_file))
156
621
  config["authorized_users"] ||= []
157
622
 
158
- # Don't add if already present
159
623
  existing = config["authorized_users"].find { |u| u["id"] == user_id || u["name"]&.downcase == display_name.downcase }
160
624
  if existing
161
625
  puts " ✓ #{display_name} already in authorized_users (id: #{existing["id"]})"
@@ -92,7 +92,7 @@ module Brainiac
92
92
  user && user["human"]
93
93
  end
94
94
 
95
- def identify_project_by_tags(tags)
95
+ def identify_project_by_tags(tags, board_key: nil)
96
96
  tag_names = tags.map { |t| t.is_a?(Hash) ? t["name"] : t.to_s }.map(&:downcase)
97
97
 
98
98
  PROJECTS.each do |key, config|
@@ -100,7 +100,21 @@ module Brainiac
100
100
  return [key, config] if tag_names.intersect?(project_tags)
101
101
  end
102
102
 
103
- # Fall back to default project
103
+ # Fall back to board's default_project if configured
104
+ if board_key
105
+ board_config = @boards[board_key]
106
+ if board_config && board_config["default_project"]
107
+ project_key = board_config["default_project"]
108
+ return [project_key, PROJECTS[project_key]] if PROJECTS.key?(project_key)
109
+ end
110
+
111
+ # Fall back to any project whose fizzy_board matches this board_key
112
+ PROJECTS.each do |key, config|
113
+ return [key, config] if config["fizzy_board"] == board_key
114
+ end
115
+ end
116
+
117
+ # Fall back to global default project
104
118
  default_key = default_project_key
105
119
  default_key ? [default_key, PROJECTS[default_key]] : nil
106
120
  end
@@ -72,8 +72,8 @@ def verify_signature!(request, payload_body, board_key: nil)
72
72
  end
73
73
 
74
74
  # Config delegators
75
- def identify_project_by_tags(tags)
76
- Brainiac::Plugins::Fizzy::Config.identify_project_by_tags(tags)
75
+ def identify_project_by_tags(tags, board_key: nil)
76
+ Brainiac::Plugins::Fizzy::Config.identify_project_by_tags(tags, board_key: board_key)
77
77
  end
78
78
 
79
79
  def board_config(board_key)
@@ -114,14 +114,14 @@ end
114
114
 
115
115
  # Webhook dispatch — routes incoming actions to the appropriate handler.
116
116
  # Called from within the Sinatra route block, so it must be a top-level method.
117
- def dispatch_webhook_action(action, payload)
117
+ def dispatch_webhook_action(action, payload, board_key: nil)
118
118
  case action
119
119
  when "card_assigned"
120
- handle_card_assigned(payload)
120
+ handle_card_assigned(payload, board_key: board_key)
121
121
  when "comment_created"
122
- handle_comment(payload)
122
+ handle_comment(payload, board_key: board_key)
123
123
  when "card_published", "card_triaged"
124
- Brainiac::Plugins::Fizzy.handle_publish_or_triage(action, payload)
124
+ Brainiac::Plugins::Fizzy.handle_publish_or_triage(action, payload, board_key: board_key)
125
125
  else
126
126
  LOG.info "[Fizzy] Ignoring unknown action: #{action}"
127
127
  [200, { status: "ignored", action: action }.to_json]
@@ -5,7 +5,7 @@
5
5
  # When a card is assigned to a local agent, creates a worktree, builds the prompt,
6
6
  # and dispatches the agent to begin work.
7
7
 
8
- def handle_card_assigned(payload)
8
+ def handle_card_assigned(payload, board_key: nil)
9
9
  eventable = payload["eventable"] || {}
10
10
  assignees = eventable["assignees"] || []
11
11
 
@@ -23,10 +23,10 @@ def handle_card_assigned(payload)
23
23
  title = eventable["title"] || "untitled"
24
24
  tags = eventable["tags"] || []
25
25
 
26
- project_result = identify_project_by_tags(tags)
26
+ project_result = identify_project_by_tags(tags, board_key: board_key)
27
27
  unless project_result
28
28
  tag_names = tags.map { |t| t.is_a?(Hash) ? t["name"] : t }.join(", ")
29
- LOG.warn "No project found for card ##{card_number} with tags: #{tag_names}"
29
+ LOG.warn "No project found for card ##{card_number} with tags: #{tag_names} (board: #{board_key})"
30
30
  return [200, { status: "ignored", reason: "no matching project" }.to_json]
31
31
  end
32
32
 
@@ -21,7 +21,7 @@ CommentContext = Struct.new(
21
21
  keyword_init: true
22
22
  )
23
23
 
24
- def handle_comment(payload)
24
+ def handle_comment(payload, board_key: nil)
25
25
  eventable = payload["eventable"] || {}
26
26
  plain_text = eventable.dig("body", "plain_text") || ""
27
27
  card_internal_id = eventable.dig("card", "id")
@@ -44,7 +44,7 @@ def handle_comment(payload)
44
44
  card_info = lookup_fizzy_card_info(card_internal_id)
45
45
  comment_id = eventable["id"]
46
46
 
47
- project_config, project_key = resolve_fizzy_project(card_info, card_internal_id, eventable)
47
+ project_config, project_key = resolve_fizzy_project(card_info, card_internal_id, eventable, board_key: board_key)
48
48
  return [200, { status: "ignored", reason: "no matching project" }.to_json] unless project_config
49
49
 
50
50
  tags = parse_inline_tags(plain_text)
@@ -285,14 +285,14 @@ def handle_cancel_command(eventable, card_internal_id)
285
285
  [200, { status: "cancelled", card: card_number_for_cancel || card_internal_id, sessions_killed: killed }.to_json]
286
286
  end
287
287
 
288
- def resolve_fizzy_project(card_info, card_internal_id, eventable)
288
+ def resolve_fizzy_project(card_info, card_internal_id, eventable, board_key: nil)
289
289
  if card_info
290
290
  if card_info["project"]
291
291
  project_key = card_info["project"]
292
292
  project_config = PROJECTS[project_key] || DEFAULT_PROJECT
293
293
  else
294
294
  card_tags = eventable.dig("card", "tags") || []
295
- project_result = identify_project_by_tags(card_tags)
295
+ project_result = identify_project_by_tags(card_tags, board_key: board_key)
296
296
  if project_result
297
297
  project_key, project_config = project_result
298
298
  card_info["project"] = project_key
@@ -305,7 +305,7 @@ def resolve_fizzy_project(card_info, card_internal_id, eventable)
305
305
  end
306
306
  else
307
307
  card_tags = eventable.dig("card", "tags") || []
308
- project_result = identify_project_by_tags(card_tags)
308
+ project_result = identify_project_by_tags(card_tags, board_key: board_key)
309
309
  if project_result
310
310
  project_key, project_config = project_result
311
311
  else
@@ -5,7 +5,7 @@
5
5
  # When a new card is created, checks for similar existing cards using
6
6
  # trigram and semantic similarity. Posts a warning comment if duplicates found.
7
7
 
8
- def handle_card_published(payload)
8
+ def handle_card_published(payload, board_key: nil)
9
9
  eventable = payload["eventable"] || {}
10
10
  card_number = eventable["number"]
11
11
  title = eventable["title"] || ""
@@ -336,9 +336,38 @@ module Brainiac
336
336
  LOG.info "[Fizzy:CardIndex] Starting background backfill..."
337
337
  CARD_INDEX.backfill
338
338
  end
339
+
340
+ # Auto-reactivate deactivated webhooks in background
341
+ Thread.new { check_and_reactivate_webhooks }
339
342
  end
340
343
  end
341
344
 
345
+ def check_and_reactivate_webhooks
346
+ boards = Config.boards
347
+ return if boards.empty?
348
+
349
+ env = Helpers.default_fizzy_env
350
+ boards.each do |board_key, board_config|
351
+ board_id = board_config["board_id"]
352
+ next unless board_id
353
+
354
+ output = run_cmd("fizzy", "webhook", "list", "--board", board_id, "--all", "--json", env: env)
355
+ webhooks = JSON.parse(output)["data"] || []
356
+
357
+ webhooks.each do |webhook|
358
+ next unless webhook["name"]&.start_with?("brainiac")
359
+ next if webhook["active"]
360
+
361
+ LOG.info "[Fizzy] Reactivating webhook '#{webhook["name"]}' on board '#{board_key}' (was inactive)"
362
+ run_cmd("fizzy", "webhook", "reactivate", webhook["id"], "--board", board_id, env: env)
363
+ end
364
+ rescue StandardError => e
365
+ LOG.warn "[Fizzy] Could not check webhooks for board '#{board_key}': #{e.message}"
366
+ end
367
+ rescue StandardError => e
368
+ LOG.warn "[Fizzy] Webhook reactivation check failed: #{e.message}"
369
+ end
370
+
342
371
  # Detect CLI provider from Fizzy card tags (e.g., cli-grok tag)
343
372
  def register_detect_cli_provider
344
373
  Brainiac.on(:detect_cli_provider) do |ctx|
@@ -23,8 +23,12 @@ module Brainiac
23
23
  ### Screenshots (MANDATORY for UI changes)
24
24
  If you touched any `.js`, `.jsx`, `.css`, or `.html` in a web app directory and `./scripts/screenshot-page.sh` exists, screenshot every affected page.
25
25
 
26
- ### Retrieving Full Comment Text
27
- If you need the full text of a truncated comment, run: `fizzy comment show COMMENT_ID --card CARD_NUMBER`
26
+ ### Fizzy CLI Tips
27
+ - Use `--jq` for filtering output (built-in, never pipe to external jq): `fizzy card list --jq '[.data[] | {number, title}]'`
28
+ - Use `fizzy search "query"` for cross-board full-text search
29
+ - Use `fizzy card list --search "term"` for structured filtering within a board
30
+ - Use `--agent` flag for raw JSON data without envelope when parsing output
31
+ - Retrieve full comment text: `fizzy comment show COMMENT_ID --card CARD_NUMBER`
28
32
 
29
33
  ### Card Memory Discipline (CRITICAL for long-running cards)
30
34
  When writing your memory file for a Fizzy card session, include:
@@ -40,8 +44,8 @@ module Brainiac
40
44
  Re-fetch the card to see if anything changed while you were working:
41
45
 
42
46
  ```bash
43
- fizzy card show {{CARD_NUMBER}}
44
- fizzy comment list --card {{CARD_NUMBER}}
47
+ fizzy card show {{CARD_NUMBER}} --jq '.data | {title, description, assignees: [.assignees[].name]}'
48
+ fizzy comment list --card {{CARD_NUMBER}} --jq '[.data[-5:] | .[] | {creator: .creator.name, body: .body.plain_text}]'
45
49
  ```
46
50
 
47
51
  Compare against the card context from the start of your session. Check for:
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Fizzy
6
- VERSION = "0.0.16"
6
+ VERSION = "0.0.18"
7
7
  end
8
8
  end
9
9
  end
@@ -78,7 +78,7 @@ module Brainiac
78
78
  reload_projects!
79
79
  reload_agent_registry!
80
80
 
81
- status_code, body = dispatch_webhook_action(action, payload)
81
+ status_code, body = dispatch_webhook_action(action, payload, board_key: board_key)
82
82
  LOG.info "[Fizzy] #{action} response: #{status_code} - #{body}"
83
83
  halt status_code, body
84
84
  rescue JSON::ParserError => e
@@ -175,7 +175,7 @@ module Brainiac
175
175
 
176
176
  public
177
177
 
178
- def handle_publish_or_triage(action, payload)
178
+ def handle_publish_or_triage(action, payload, board_key: nil)
179
179
  eventable = payload["eventable"] || {}
180
180
  card_number = eventable["number"]&.to_s
181
181
 
@@ -189,10 +189,10 @@ module Brainiac
189
189
 
190
190
  if action == "card_published"
191
191
  assignees = eventable["assignees"] || []
192
- return handle_card_assigned(payload) if assignees.any? { |a| local_agent_names.include?(a["name"]) }
192
+ return handle_card_assigned(payload, board_key: board_key) if assignees.any? { |a| local_agent_names.include?(a["name"]) }
193
193
  end
194
194
 
195
- handle_card_published(payload)
195
+ handle_card_published(payload, board_key: board_key)
196
196
  end
197
197
  end
198
198
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac-fizzy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.16
4
+ version: 0.0.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis