brainiac 0.0.23 → 0.0.25

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: 8bdf84241f89389aef9a1693abd00b1cfdf77d645c096081acb4ea0e8d1de8ed
4
- data.tar.gz: 9ed60b3c1ba3fdcdcae4e3b05cfb15fa2a1a60c6311cd8add2c7fa9a3e4e839a
3
+ metadata.gz: 0a843b412b04f2b176f13e248bcea6327eb27c2893ed516a566c9f79e3b9dbc4
4
+ data.tar.gz: 0bc50a0db4b6e8d5ea67dbf3de47d8481e94a64d886a378730d4d27da5185403
5
5
  SHA512:
6
- metadata.gz: 7e5df006c01b492f2e1e9f9918a208b2bcdbdefcf0df032662b12d2f107872f1456ba37200ad70ebd19d3fa218da030a35681249ec4f232af56da4132bc7eef8
7
- data.tar.gz: 24cc2728b08071c0ecc4c52bbabedf568446749322e27aad386c5b23395affce9e6b1c16c06feace6ba8487d261dc136a64db6325de1d75f1efc0ca320bef5f4
6
+ metadata.gz: b3aa491acebfe3b08aef05735fb593c6835b3757b2502f0ca47ad765aca21d9357a0fbb52c8d0a8f0103ad5aff1ff855d2159ff056297dfa4d7ecfeeb2f4387a
7
+ data.tar.gz: 54179e3366fb8246ed4221e82e70edc67ac0a24fa05212011624f3b9997f11cbdcc481c49b0141efa4c3c8cfbcbb23ffd9f20286442f2a0fb96557ccc8308c12
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brainiac (0.0.23)
4
+ brainiac (0.0.25)
5
5
  puma (~> 7.2)
6
6
  rackup (~> 2.3)
7
7
  sinatra (~> 4.1)
@@ -84,7 +84,7 @@ DEPENDENCIES
84
84
  CHECKSUMS
85
85
  ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
86
86
  base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
87
- brainiac (0.0.23)
87
+ brainiac (0.0.25)
88
88
  json (2.19.9) sha256=9b9025b7cdddafa38d316eca0b2358488e42d417045c1b90d216a9fefe46b79a
89
89
  language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
90
90
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
data/bin/brainiac CHANGED
@@ -527,6 +527,84 @@ def generate_plugin_skill_md(plugin_name, pascal_name)
527
527
  SKILL
528
528
  end
529
529
 
530
+ SCREENSHOT_CREDS_FILE = File.join(BRAINIAC_DIR, "tmp", "screenshot-creds.json")
531
+
532
+ # Resolve all op:// references in deployments.json and write to a secure cache file.
533
+ # This allows screenshot commands to work without re-authenticating with 1Password.
534
+ def resolve_screenshot_credentials(quiet: false)
535
+ deployments_file = File.join(BRAINIAC_DIR, "deployments.json")
536
+ unless File.exist?(deployments_file)
537
+ puts " ⚠ No deployments.json found, skipping credential resolution." unless quiet
538
+ return false
539
+ end
540
+
541
+ deployments = JSON.parse(File.read(deployments_file))
542
+ environments = deployments["environments"] || {}
543
+
544
+ # Collect all unique op:// references
545
+ op_refs = {}
546
+ environments.each_value do |env_config|
547
+ screenshot = env_config["screenshot"]
548
+ next unless screenshot
549
+
550
+ (screenshot["customers"] || {}).each_value { |ref| op_refs[ref] = nil }
551
+ (screenshot["employees"] || {}).each_value { |ref| op_refs[ref] = nil }
552
+ end
553
+
554
+ if op_refs.empty?
555
+ puts " ⚠ No screenshot credentials to resolve." unless quiet
556
+ return false
557
+ end
558
+
559
+ puts " Resolving #{op_refs.size} credential(s) from 1Password..." unless quiet
560
+
561
+ # Resolve each unique op:// reference
562
+ failed = []
563
+ op_refs.each_key do |ref|
564
+ password, status = Open3.capture2("op", "read", ref)
565
+ if status.success?
566
+ op_refs[ref] = password.chomp
567
+ else
568
+ failed << ref
569
+ end
570
+ end
571
+
572
+ if failed.any?
573
+ puts " ❌ Failed to resolve #{failed.size} credential(s):" unless quiet
574
+ failed.each { |ref| puts " #{ref}" } unless quiet
575
+ return false
576
+ end
577
+
578
+ # Write resolved credentials to cache file with restrictive permissions
579
+ cache_dir = File.join(BRAINIAC_DIR, "tmp")
580
+ FileUtils.mkdir_p(cache_dir)
581
+ File.write(SCREENSHOT_CREDS_FILE, JSON.pretty_generate(op_refs))
582
+ File.chmod(0o600, SCREENSHOT_CREDS_FILE)
583
+
584
+ puts " ✓ Screenshot credentials cached (#{op_refs.size} resolved)" unless quiet
585
+ true
586
+ rescue JSON::ParserError => e
587
+ puts " ❌ Failed to parse deployments.json: #{e.message}" unless quiet
588
+ false
589
+ end
590
+
591
+ # Read a resolved credential from the cache file, falling back to `op read`.
592
+ def read_screenshot_credential(op_ref)
593
+ # Try cache first
594
+ if File.exist?(SCREENSHOT_CREDS_FILE)
595
+ creds = JSON.parse(File.read(SCREENSHOT_CREDS_FILE))
596
+ return creds[op_ref] if creds[op_ref]
597
+ end
598
+
599
+ # Fallback: resolve directly via op read
600
+ password, status = Open3.capture2("op", "read", op_ref)
601
+ return password.chomp if status.success?
602
+
603
+ nil
604
+ rescue JSON::ParserError
605
+ nil
606
+ end
607
+
530
608
  def start_server(daemon: false)
531
609
  # Resolve the real path of the brainiac script (follows symlinks)
532
610
  receiver_path = File.join(BRAINIAC_ROOT, "receiver.rb")
@@ -544,6 +622,9 @@ def start_server(daemon: false)
544
622
  exit 1
545
623
  end
546
624
 
625
+ # Pre-resolve screenshot credentials from 1Password (so agents can screenshot without re-auth)
626
+ resolve_screenshot_credentials
627
+
547
628
  if daemon
548
629
  # Daemon mode: start in background (like the old behavior)
549
630
  log_dir = File.join(receiver_dir, "tmp")
@@ -1107,8 +1188,7 @@ when "unregister", "remove", "rm"
1107
1188
  when "list", "ls"
1108
1189
  list_projects
1109
1190
 
1110
- when "projects"
1111
- # Support "brainiac projects list" as an alias for "brainiac list"
1191
+ when "projects", "project"
1112
1192
  projects_cmd = ARGV.shift
1113
1193
  case projects_cmd
1114
1194
  when "list", "ls", nil
@@ -1140,9 +1220,140 @@ when "projects"
1140
1220
  save_projects(projects)
1141
1221
 
1142
1222
  puts "✓ Set '#{project_key}' as the default project"
1223
+ when "update", "set"
1224
+ project_key = ARGV.shift
1225
+ field = ARGV.shift
1226
+ value = ARGV.join(" ") # Allow multi-word values (e.g. tags with spaces)
1227
+
1228
+ unless project_key && field
1229
+ puts "Usage: brainiac projects update <key> <field> <value>"
1230
+ puts ""
1231
+ puts "Fields:"
1232
+ puts " repo_path <path> Path to the project repository"
1233
+ puts " github_repo <owner/repo> GitHub repository (e.g. stowzilla/brainiac)"
1234
+ puts " fizzy_tags <tag1,tag2> Comma-separated Fizzy tags"
1235
+ puts " fizzy_board <board> Default Fizzy board key"
1236
+ puts " cli_provider <provider> CLI provider name (e.g. kiro, grok)"
1237
+ puts " agent_name <name> Default agent for this project"
1238
+ puts " agent_model <model> Default model (e.g. auto, opus, sonnet)"
1239
+ puts " default_branch <branch> Default git branch (e.g. main, master)"
1240
+ puts ""
1241
+ puts "Examples:"
1242
+ puts " brainiac projects update brainiac fizzy_board stowzilla"
1243
+ puts " brainiac projects update marketplace cli_provider grok"
1244
+ puts " brainiac projects update brainiac fizzy_tags brainiac,core"
1245
+ puts " brainiac projects update marketplace agent_model opus"
1246
+ exit 1
1247
+ end
1248
+
1249
+ projects = load_projects
1250
+ unless projects.key?(project_key)
1251
+ puts "Error: Project '#{project_key}' not found."
1252
+ puts "Available projects: #{projects.keys.join(", ")}"
1253
+ exit 1
1254
+ end
1255
+
1256
+ # Handle special field parsing
1257
+ case field
1258
+ when "fizzy_tags", "tags"
1259
+ field = "fizzy_tags"
1260
+ unless value && !value.empty?
1261
+ puts "Error: Value required. Provide comma-separated tags."
1262
+ exit 1
1263
+ end
1264
+ parsed_value = value.split(",").map(&:strip)
1265
+ when "repo_path"
1266
+ unless value && !value.empty?
1267
+ puts "Error: Value required."
1268
+ exit 1
1269
+ end
1270
+ parsed_value = File.expand_path(value)
1271
+ puts "Warning: Path does not exist: #{parsed_value}" unless Dir.exist?(parsed_value)
1272
+ else
1273
+ # Handles known fields (github_repo, fizzy_board, cli_provider, agent_name, agent_model,
1274
+ # default_branch) and arbitrary fields for forward-compatibility
1275
+ unless value && !value.empty?
1276
+ puts "Error: Value required."
1277
+ exit 1
1278
+ end
1279
+ parsed_value = value
1280
+ end
1281
+
1282
+ projects[project_key][field] = parsed_value
1283
+ save_projects(projects)
1284
+
1285
+ display_value = parsed_value.is_a?(Array) ? parsed_value.join(", ") : parsed_value
1286
+ puts "✓ Updated #{project_key}: #{field} = #{display_value}"
1287
+
1288
+ when "unset", "delete-field"
1289
+ project_key = ARGV.shift
1290
+ field = ARGV.shift
1291
+
1292
+ unless project_key && field
1293
+ puts "Usage: brainiac projects unset <key> <field>"
1294
+ puts ""
1295
+ puts "Removes a field from a project's configuration."
1296
+ puts ""
1297
+ puts "Examples:"
1298
+ puts " brainiac projects unset marketplace fizzy_board"
1299
+ puts " brainiac projects unset brainiac default_branch"
1300
+ exit 1
1301
+ end
1302
+
1303
+ projects = load_projects
1304
+ unless projects.key?(project_key)
1305
+ puts "Error: Project '#{project_key}' not found."
1306
+ exit 1
1307
+ end
1308
+
1309
+ unless projects[project_key].key?(field)
1310
+ puts "Error: Field '#{field}' not set on project '#{project_key}'."
1311
+ exit 1
1312
+ end
1313
+
1314
+ projects[project_key].delete(field)
1315
+ save_projects(projects)
1316
+ puts "✓ Removed #{field} from #{project_key}"
1317
+
1318
+ when "help", "--help", "-h"
1319
+ puts <<~HELP
1320
+ Usage: brainiac projects <command> [options]
1321
+
1322
+ Commands:
1323
+ list List all registered projects (default)
1324
+ show <key> Show detailed configuration for a project
1325
+ default <key> Set the default project (fallback when no tags match)
1326
+ update <key> <field> <value> Update a project config field
1327
+ unset <key> <field> Remove a field from project config
1328
+
1329
+ Aliases:
1330
+ brainiac project Works the same as 'brainiac projects'
1331
+ brainiac list Shortcut for 'brainiac projects list'
1332
+ brainiac show Shortcut for 'brainiac projects show'
1333
+
1334
+ Update fields:
1335
+ repo_path Path to the project repository
1336
+ github_repo GitHub repository (owner/repo)
1337
+ fizzy_tags Comma-separated Fizzy tags
1338
+ fizzy_board Default Fizzy board key
1339
+ cli_provider CLI provider name (kiro, grok, etc.)
1340
+ agent_name Default agent for this project
1341
+ agent_model Default model (auto, opus, sonnet, haiku, etc.)
1342
+ default_branch Default git branch
1343
+
1344
+ Examples:
1345
+ brainiac projects list
1346
+ brainiac project show marketplace
1347
+ brainiac projects default marketplace
1348
+ brainiac projects update brainiac fizzy_board stowzilla
1349
+ brainiac projects update marketplace agent_model opus
1350
+ brainiac projects update brainiac fizzy_tags brainiac,core
1351
+ brainiac projects unset marketplace fizzy_board
1352
+ HELP
1143
1353
  else
1144
1354
  puts "Unknown projects command: #{projects_cmd}"
1145
- puts "Available: list, show, default"
1355
+ puts "Available: list, show, default, update, unset, help"
1356
+ puts "Run 'brainiac projects help' for more details."
1146
1357
  exit 1
1147
1358
  end
1148
1359
 
@@ -3173,6 +3384,183 @@ when "plugin"
3173
3384
  puts " brainiac plugins List installed plugins"
3174
3385
  end
3175
3386
 
3387
+ when "screenshot"
3388
+ # Resolve screenshot credentials from cached file and delegate to project's screenshot script.
3389
+ # Usage: brainiac screenshot [app-name] [page-path] [--env ENV] [extra args...]
3390
+ #
3391
+ # Credentials are resolved from 1Password at server start and cached in
3392
+ # ~/.brainiac/tmp/screenshot-creds.json. Falls back to `op read` if cache is missing.
3393
+ # Run `brainiac screenshot resolve` to manually refresh the cache.
3394
+
3395
+ deployments_file = File.join(BRAINIAC_DIR, "deployments.json")
3396
+ abort "Error: #{deployments_file} not found. Run 'brainiac setup' or create it manually." unless File.exist?(deployments_file)
3397
+
3398
+ deployments = JSON.parse(File.read(deployments_file))
3399
+ environments = deployments["environments"] || {}
3400
+
3401
+ sub = ARGV.shift
3402
+ case sub
3403
+ when "envs", "list"
3404
+ environments.each do |key, env|
3405
+ screenshot = env["screenshot"]
3406
+ if screenshot
3407
+ customers = (screenshot["customers"] || {}).keys
3408
+ employees = (screenshot["employees"] || {}).keys
3409
+ puts " #{key} (#{env["label"]})"
3410
+ puts " Customers: #{customers.join(", ")}" if customers.any?
3411
+ puts " Employees: #{employees.join(", ")}" if employees.any?
3412
+ else
3413
+ puts " #{key} (#{env["label"]}) — no screenshot config"
3414
+ end
3415
+ end
3416
+
3417
+ when "resolve"
3418
+ puts "Resolving screenshot credentials from 1Password..."
3419
+ if resolve_screenshot_credentials(quiet: false)
3420
+ puts "\nCredentials cached at #{SCREENSHOT_CREDS_FILE}"
3421
+ puts "Screenshots will use the cache — no 1Password re-auth needed."
3422
+ else
3423
+ abort "\nFailed to resolve credentials. Ensure `op` is signed in."
3424
+ end
3425
+
3426
+ when "help", "--help", "-h", nil
3427
+ puts <<~HELP
3428
+ brainiac screenshot — Take screenshots with credentials from deployments.json
3429
+
3430
+ Usage:
3431
+ brainiac screenshot <app-name> <page-path> [options]
3432
+
3433
+ Credentials are resolved from 1Password at server start and cached in
3434
+ #{SCREENSHOT_CREDS_FILE} (mode 0600). Use `brainiac screenshot resolve`
3435
+ to manually refresh the cache.
3436
+
3437
+ Options:
3438
+ --env ENV Environment to use (default: auto-detect from project owner)
3439
+ --customer EMAIL Use specific customer email (default: first in list)
3440
+ --employee EMAIL Use specific employee email (default: first in list)
3441
+ --project KEY Project key (default: current directory or default project)
3442
+ All other args are passed through to screenshot-page.sh
3443
+
3444
+ Commands:
3445
+ brainiac screenshot resolve Resolve and cache credentials from 1Password
3446
+ brainiac screenshot envs List environments with screenshot config
3447
+ brainiac screenshot help Show this help
3448
+
3449
+ Examples:
3450
+ brainiac screenshot ops-app /api-explorer
3451
+ brainiac screenshot ops-app /inventory --env dev04
3452
+ brainiac screenshot app /dashboard --env dev02
3453
+ HELP
3454
+
3455
+ else
3456
+ # Parse flags from remaining args
3457
+ app_name = sub
3458
+ page_path = ARGV.shift
3459
+ abort "Error: page path required. Usage: brainiac screenshot <app-name> <page-path> [options]" unless page_path
3460
+
3461
+ env_name = nil
3462
+ customer_email = nil
3463
+ employee_email = nil
3464
+ project_key = nil
3465
+ passthrough_args = []
3466
+
3467
+ while (arg = ARGV.shift)
3468
+ case arg
3469
+ when "--env"
3470
+ env_name = ARGV.shift
3471
+ when "--customer"
3472
+ customer_email = ARGV.shift
3473
+ when "--employee"
3474
+ employee_email = ARGV.shift
3475
+ when "--project"
3476
+ project_key = ARGV.shift
3477
+ else
3478
+ passthrough_args << arg
3479
+ # If the flag takes a value, pass it through too
3480
+ passthrough_args << ARGV.shift if arg.start_with?("--") && !arg.include?("=") && ARGV.first && !ARGV.first.start_with?("--")
3481
+ end
3482
+ end
3483
+
3484
+ # Determine environment (default: dev02 for Linux)
3485
+ env_name ||= case RUBY_PLATFORM
3486
+ when /darwin/ then "dev01"
3487
+ else "dev02"
3488
+ end
3489
+
3490
+ env_config = environments[env_name]
3491
+ abort "Error: Environment '#{env_name}' not found in deployments.json. Available: #{environments.keys.join(", ")}" unless env_config
3492
+
3493
+ screenshot_config = env_config["screenshot"]
3494
+ abort "Error: No 'screenshot' config for environment '#{env_name}' in deployments.json." unless screenshot_config
3495
+
3496
+ customers = screenshot_config["customers"] || {}
3497
+ employees = screenshot_config["employees"] || {}
3498
+
3499
+ # Resolve which email to use
3500
+ if app_name == "ops-app"
3501
+ email = employee_email || employees.keys.first
3502
+ op_ref = employees[email]
3503
+ abort "Error: Employee email '#{email}' not found in #{env_name} screenshot config. Available: #{employees.keys.join(", ")}" unless op_ref
3504
+ else
3505
+ email = customer_email || customers.keys.first
3506
+ op_ref = customers[email]
3507
+ abort "Error: Customer email '#{email}' not found in #{env_name} screenshot config. Available: #{customers.keys.join(", ")}" unless op_ref
3508
+ end
3509
+
3510
+ # Resolve password (from cache or 1Password directly)
3511
+ password = read_screenshot_credential(op_ref)
3512
+ unless password
3513
+ abort "Error: Failed to resolve password for #{op_ref}. Run 'brainiac screenshot resolve' or 'brainiac restart' to refresh the cache."
3514
+ end
3515
+
3516
+ # Determine project path
3517
+ projects = load_projects
3518
+ if project_key
3519
+ project = projects[project_key]
3520
+ abort "Error: Project '#{project_key}' not found." unless project
3521
+ else
3522
+ # Try to find project from current directory or env config
3523
+ project = projects.find { |_k, p| p["repo_path"] == Dir.pwd }&.last
3524
+ project ||= projects[env_config["project"]]
3525
+ end
3526
+
3527
+ abort "Error: Could not determine project. Use --project KEY." unless project
3528
+ repo_path = project["repo_path"]
3529
+ script_path = File.join(repo_path, "scripts", "screenshot-page.sh")
3530
+ abort "Error: Screenshot script not found at #{script_path}" unless File.exist?(script_path)
3531
+
3532
+ # Build environment
3533
+ screenshot_env = {
3534
+ "SCREENSHOT_CUSTOMER_EMAIL" => (app_name == "ops-app" ? (customers.keys.first || email) : email),
3535
+ "SCREENSHOT_CUSTOMER_PASSWORD" => password,
3536
+ "SCREENSHOT_OPS_EMAIL" => (app_name == "ops-app" ? email : (employees.keys.first || email)),
3537
+ "SCREENSHOT_OPS_PASSWORD" => password
3538
+ }
3539
+
3540
+ # Also set environment-specific email vars
3541
+ env_upper = env_name.upcase.tr("-", "_")
3542
+ screenshot_env["SCREENSHOT_#{env_upper}_CUSTOMER_EMAIL"] = customers.keys.first if customers.any?
3543
+
3544
+ # Run the screenshot script
3545
+ cmd = [script_path, app_name, page_path, "", ""] + passthrough_args
3546
+ puts "Taking screenshot: #{app_name} #{page_path} (env: #{env_name}, user: #{email})"
3547
+ puts "Running: #{cmd.join(" ")}"
3548
+
3549
+ pid = spawn(screenshot_env, *cmd, chdir: repo_path)
3550
+ Process.wait(pid)
3551
+
3552
+ if $CHILD_STATUS.success?
3553
+ # Find the most recent screenshot
3554
+ attachments_dir = File.join(repo_path, ".fizzy-attachments")
3555
+ if Dir.exist?(attachments_dir)
3556
+ latest = Dir.glob(File.join(attachments_dir, "*.png")).max_by { |f| File.mtime(f) }
3557
+ puts "\n✅ Screenshot saved: #{latest}" if latest
3558
+ end
3559
+ else
3560
+ abort "\n❌ Screenshot failed (exit #{$CHILD_STATUS.exitstatus})"
3561
+ end
3562
+ end
3563
+
3176
3564
  when "version", "--version", "-v"
3177
3565
  puts "brainiac #{BRAINIAC_VERSION}"
3178
3566
 
@@ -3200,8 +3588,8 @@ when "help", "--help", "-h", nil
3200
3588
  brainiac register [options] Register current directory as a project
3201
3589
  brainiac unregister <key> Unregister a project
3202
3590
  brainiac list List all registered projects
3203
- brainiac projects list List all registered projects (alias)
3204
- brainiac projects default <key> Set the default project
3591
+ brainiac projects <command> Manage projects (list, show, default, help)
3592
+ brainiac project <command> Alias for 'brainiac projects'
3205
3593
  brainiac show <key> Show project configuration
3206
3594
  brainiac plugin new <name> Generate a new plugin gem skeleton
3207
3595
  brainiac update Update brainiac and all installed plugins
@@ -3214,6 +3602,7 @@ when "help", "--help", "-h", nil
3214
3602
  brainiac provider <command> Manage CLI providers
3215
3603
  brainiac role <command> Manage agent roles
3216
3604
  brainiac agent <command> Manage agent registry (env, list, show)
3605
+ brainiac screenshot <app> <path> Take screenshot with 1Password credentials
3217
3606
  brainiac config Configure Brainiac CLI
3218
3607
  brainiac path Show Brainiac config directory
3219
3608
  brainiac version Show version
@@ -9,7 +9,7 @@ _brainiac() {
9
9
  local brainiac_dir="${BRAINIAC_DIR:-$HOME/.brainiac}"
10
10
 
11
11
  # Top-level commands (built-in + installed plugins)
12
- local commands="server start stop restart logs status register unregister list show brain cron provider role agent config path version help setup projects card-map handler plugin install uninstall plugins"
12
+ local commands="server start stop restart logs status register unregister list show brain cron provider role agent config path version help setup projects project card-map handler plugin install uninstall plugins"
13
13
 
14
14
  # Add installed plugin names as top-level commands
15
15
  if [[ -f "$brainiac_dir/plugins.json" ]]; then
@@ -180,16 +180,40 @@ _brainiac() {
180
180
  esac
181
181
  ;;
182
182
 
183
- projects)
183
+ projects|project)
184
184
  case $cword in
185
185
  2)
186
- COMPREPLY=($(compgen -W "list default" -- "$cur"))
186
+ COMPREPLY=($(compgen -W "list show default update unset help" -- "$cur"))
187
187
  ;;
188
188
  3)
189
- if [[ "${words[2]}" == "default" ]]; then
189
+ local subcmd="${words[2]}"
190
+ if [[ "$subcmd" == "default" || "$subcmd" == "show" || "$subcmd" == "update" || "$subcmd" == "unset" ]]; then
190
191
  COMPREPLY=($(compgen -W "$(_brainiac_projects)" -- "$cur"))
191
192
  fi
192
193
  ;;
194
+ 4)
195
+ local subcmd="${words[2]}"
196
+ if [[ "$subcmd" == "update" || "$subcmd" == "unset" ]]; then
197
+ COMPREPLY=($(compgen -W "repo_path github_repo fizzy_tags fizzy_board cli_provider agent_name agent_model default_branch" -- "$cur"))
198
+ fi
199
+ ;;
200
+ 5)
201
+ local subcmd="${words[2]}"
202
+ local field="${words[4]}"
203
+ if [[ "$subcmd" == "update" ]]; then
204
+ case "$field" in
205
+ cli_provider)
206
+ COMPREPLY=($(compgen -W "$(_brainiac_providers)" -- "$cur"))
207
+ ;;
208
+ agent_model)
209
+ COMPREPLY=($(compgen -W "auto opus sonnet haiku deepseek minimax qwen" -- "$cur"))
210
+ ;;
211
+ agent_name)
212
+ COMPREPLY=($(compgen -W "$(_brainiac_agents)" -- "$cur"))
213
+ ;;
214
+ esac
215
+ fi
216
+ ;;
193
217
  esac
194
218
  ;;
195
219
 
@@ -157,7 +157,7 @@ DEFAULT_PROJECT = {
157
157
  "agent_effort_flag" => ENV["AGENT_EFFORT_FLAG"] || "--effort",
158
158
  "agent_effort" => ENV.fetch("AGENT_EFFORT", nil),
159
159
  "allowed_models" => {
160
- "opus" => "claude-opus-4.6",
160
+ "opus" => "claude-opus-4.5",
161
161
  "sonnet" => "claude-sonnet-4.6",
162
162
  "haiku" => "claude-haiku-4.5",
163
163
  "deepseek" => "deepseek-3.2",
data/lib/brainiac/cron.rb CHANGED
@@ -466,7 +466,10 @@ def handle_cron_completion(job, project, agent_name, agent_config_name, log_file
466
466
  update_cron_job_state(job)
467
467
 
468
468
  if File.exist?(response_file)
469
- LOG.info "[Cron] Job #{job[:id]} completed. Response: #{File.read(response_file)[0..100]}..."
469
+ response_content = File.read(response_file).strip
470
+ LOG.info "[Cron] Job #{job[:id]} completed. Response: #{response_content[0..100]}..."
471
+
472
+ notify_cron_output(job, response_content, agent_name: agent_name) if job[:notify_target] && !response_content.empty?
470
473
  else
471
474
  LOG.warn "[Cron] Job #{job[:id]} produced no response"
472
475
  end
@@ -106,6 +106,40 @@ def run_project_hook(repo_path, hook_name, extra_env: {})
106
106
  end
107
107
  end
108
108
 
109
+ # Resolve the base branch for a new worktree.
110
+ # Emits :resolve_base_branch hook — plugins can override the default (origin/main).
111
+ # Returns a ref string (e.g., "origin/main", "epic/my-feature").
112
+ #
113
+ # @param repo_path [String] Path to the git repo
114
+ # @param card_number [Integer, String, nil] Fizzy card number (if applicable)
115
+ # @param project_key [String, nil] Brainiac project key
116
+ # @return [String] Base ref for the worktree
117
+ def resolve_base_branch(repo_path:, card_number: nil, project_key: nil)
118
+ results = Brainiac.emit(:resolve_base_branch,
119
+ repo_path: repo_path, card_number: card_number, project_key: project_key)
120
+ # First non-nil result wins (plugins return a branch name or nil to skip)
121
+ custom = results.compact.first
122
+ if custom
123
+ LOG.info "Using custom base branch '#{custom}' (from plugin hook)"
124
+ custom
125
+ else
126
+ "origin/#{get_default_branch(repo_path)}"
127
+ end
128
+ end
129
+
130
+ # Resolve the PR target branch for a card.
131
+ # Emits :resolve_pr_target hook — plugins can override the default branch.
132
+ #
133
+ # @param repo_path [String] Path to the git repo
134
+ # @param card_number [Integer, String, nil] Fizzy card number
135
+ # @param project_key [String, nil] Brainiac project key
136
+ # @return [String, nil] Target branch name (without origin/ prefix), or nil for default
137
+ def resolve_pr_target(repo_path:, card_number: nil, project_key: nil)
138
+ results = Brainiac.emit(:resolve_pr_target,
139
+ repo_path: repo_path, card_number: card_number, project_key: project_key)
140
+ results.compact.first
141
+ end
142
+
109
143
  # Create or reuse a git worktree for a given branch.
110
144
  # Returns the worktree path on success.
111
145
  def create_or_reuse_worktree(repo_path:, branch:, base_ref: nil, worktree_path: nil)
@@ -492,7 +492,11 @@ end
492
492
  def intent_skip?(message, agent_name:, source: nil, channel: nil, context: nil)
493
493
  return false unless message && agent_name && intent_config["enabled"]
494
494
 
495
+ # Some channels always require a response (e.g. PR comments are inherently directed at the agent).
496
+ bypass_channels = intent_config["bypass_channels"] || %w[github]
495
497
  intent_channel = channel || source&.to_s || "conversation"
498
+ return false if bypass_channels.any? { |bc| intent_channel.to_s.downcase.include?(bc) }
499
+
496
500
  unless check_intent(message, agent_name: agent_name, channel: intent_channel, context: context)
497
501
  LOG.info "[Intent] Skipping dispatch for #{agent_name} — message classified as not requiring response"
498
502
  return true
@@ -503,7 +507,7 @@ end
503
507
 
504
508
  def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil, effort: nil, agent_name: nil, card_number: nil, comment_id: nil,
505
509
  source: nil, source_context: {}, skip_column_move: false, cli_provider: nil, resume: false,
506
- message: nil, channel: nil, context: nil)
510
+ message: nil, channel: nil, context: nil, env: {})
507
511
  # Intent gate: if a raw message is provided, check whether the agent should respond.
508
512
  return nil if intent_skip?(message, agent_name: agent_name, source: source, channel: channel, context: context)
509
513
 
@@ -527,7 +531,7 @@ def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil
527
531
  cmd = build_agent_cmd(resolved, agent_config_name: agent_config_name, model: model, effort: effort, prompt_file: prompt_file, resume: should_resume)
528
532
  prompt_mode = resolved["prompt_mode"] || "stdin"
529
533
 
530
- spawn_env = agent_env_for(agent_name)
534
+ spawn_env = agent_env_for(agent_name).merge(env)
531
535
 
532
536
  LOG.info "Running #{resolved["agent_cli"]} in #{chdir}, logging to #{log_file}"
533
537
  LOG.info "Prompt written to #{prompt_file}"
@@ -585,7 +589,7 @@ def build_agent_cmd(resolved, agent_config_name: nil, model: nil, effort: nil, p
585
589
  # "auto" means "let the CLI choose" — skip passing it unless the provider explicitly maps it.
586
590
  if model && resolved["agent_model_flag"] && !resolved["agent_model_flag"].empty?
587
591
  allowed = resolved["allowed_models"] || {}
588
- # Pass the model if it's a mapped value (e.g. "claude-opus-4.6") or the key itself is mapped
592
+ # Pass the model if it's a mapped value (e.g. "claude-opus-4.5") or the key itself is mapped
589
593
  is_known = allowed.value?(model) || allowed.key?(model)
590
594
  cmd.push(resolved["agent_model_flag"], model) if is_known
591
595
  end
@@ -17,6 +17,8 @@
17
17
  # :build_brain_context — When building brain context (plugins add source-specific queries)
18
18
  # :pre_dispatch — Before dispatching an agent (plugins can inject config)
19
19
  # :post_comment — After an agent posts a comment/response
20
+ # :resolve_base_branch — Resolve custom base branch for worktree creation
21
+ # :resolve_pr_target — Resolve custom PR target branch (instead of default branch)
20
22
  #
21
23
  # Usage (in plugin .register):
22
24
  # Brainiac.on(:agent_completed) do |ctx|
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Brainiac
4
4
  # @return [String] the current gem version
5
- VERSION = "0.0.23"
5
+ VERSION = "0.0.25"
6
6
  end
data/monitor/shared.rb CHANGED
@@ -205,7 +205,17 @@ def deploy_bash_script(env_key, worktree:, aws_profile: nil)
205
205
  fi
206
206
  rm -f "$logfile"
207
207
  echo
208
- if [ $status -eq 0 ]; then echo "✅ Deploy complete"; else echo "❌ Deploy failed (exit $status)"; fi
208
+ if [ $status -eq 0 ]; then
209
+ echo "✅ Deploy complete"
210
+ curl -s -X POST http://localhost:4567/api/deployments/#{env_key.shellescape} \
211
+ -H "Content-Type: application/json" \
212
+ -d '{"worktree": #{worktree.to_json}, "deployed_by": "waybar"}' > /dev/null 2>&1
213
+ else
214
+ echo "❌ Deploy failed (exit $status)"
215
+ curl -s -X POST http://localhost:4567/api/deployments/#{env_key.shellescape}/failed \
216
+ -H "Content-Type: application/json" \
217
+ -d '{}' > /dev/null 2>&1
218
+ fi
209
219
  echo "Press Enter to close..."
210
220
  read
211
221
  BASH
@@ -111,7 +111,17 @@ deploy_script = <<~BASH
111
111
  fi
112
112
  rm -f "$logfile"
113
113
  echo
114
- if [ $status -eq 0 ]; then echo "✅ Deploy complete"; else echo "❌ Deploy failed (exit $status)"; fi
114
+ if [ $status -eq 0 ]; then
115
+ echo "✅ Deploy complete"
116
+ curl -s -X POST http://localhost:4567/api/deployments/#{Shellwords.escape(env_key)} \
117
+ -H "Content-Type: application/json" \
118
+ -d '{"worktree": #{worktree.to_json}, "deployed_by": "waybar"}' > /dev/null 2>&1
119
+ else
120
+ echo "❌ Deploy failed (exit $status)"
121
+ curl -s -X POST http://localhost:4567/api/deployments/#{Shellwords.escape(env_key)}/failed \
122
+ -H "Content-Type: application/json" \
123
+ -d '{}' > /dev/null 2>&1
124
+ fi
115
125
  echo
116
126
  echo "Press any key to close..."
117
127
  read -n 1
@@ -7,7 +7,7 @@
7
7
  "prompt_mode": "stdin",
8
8
  "resume_flag": "--resume",
9
9
  "models": {
10
- "opus": "claude-opus-4.6",
10
+ "opus": "claude-opus-4.5",
11
11
  "sonnet": "claude-sonnet-4.6",
12
12
  "haiku": "claude-haiku-4.5",
13
13
  "deepseek": "deepseek-3.2",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.23
4
+ version: 0.0.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis