brainiac 0.0.1 → 0.0.3
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
- checksums.yaml.gz.sig +0 -0
- data/Gemfile.lock +2 -2
- data/bin/brainiac +317 -0
- data/bin/brainiac-completion.bash +178 -0
- data/lib/brainiac/agents.rb +34 -0
- data/lib/brainiac/brain.rb +25 -0
- data/lib/brainiac/config.rb +4 -0
- data/lib/brainiac/cron.rb +9 -6
- data/lib/brainiac/handlers/discord.rb +234 -31
- data/lib/brainiac/handlers/fizzy.rb +129 -86
- data/lib/brainiac/helpers.rb +125 -41
- data/lib/brainiac/prompts.rb +97 -105
- data/lib/brainiac/version.rb +1 -1
- data/receiver.rb +15 -2
- data/templates/cli-providers/grok.json.example +15 -0
- data/templates/cli-providers/kiro.json.example +15 -0
- data/templates/roles/code-reviewer.md.example +28 -0
- data/templates/roles/general-engineer.md.example +36 -0
- data/templates/roles/test-engineer.md.example +33 -0
- data.tar.gz.sig +0 -0
- metadata +8 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ecf3d562c59eaeb76f9c1e4ceb87f3ed8c1cabb4cc6c3ddc6837e7c38fe4e8d
|
|
4
|
+
data.tar.gz: 60abd844fa6877850aaf37f603229beae693d3aa8b8d82194ac378844009e766
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab7cd6b237850d08abfb1586a45cbed1e6252196be96a1927e14b1908dab5dd6b992aaeb34a0bfed693fde8c6e296efad10b7ea2974d4daf4fc17aaaea063f5a
|
|
7
|
+
data.tar.gz: 06f90d59ed0584c9c1d94a3feac2b5ed3430c39dcc8579d4f1e18100114a8ac5feb7f3d0d1da74a128a6506da735a5b626bb072cc4457a67aa68dfe7cb196cb4
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
brainiac (0.0.
|
|
4
|
+
brainiac (0.0.3)
|
|
5
5
|
puma (~> 7.2)
|
|
6
6
|
rackup (~> 2.3)
|
|
7
7
|
sinatra (~> 4.1)
|
|
@@ -90,7 +90,7 @@ DEPENDENCIES
|
|
|
90
90
|
CHECKSUMS
|
|
91
91
|
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
92
92
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
93
|
-
brainiac (0.0.
|
|
93
|
+
brainiac (0.0.3)
|
|
94
94
|
event_emitter (0.2.6) sha256=c72697bd5cce9d36594be1972c17f1c9a573236f44303a4d1d548080364e1391
|
|
95
95
|
json (2.19.9) sha256=9b9025b7cdddafa38d316eca0b2358488e42d417045c1b90d216a9fefe46b79a
|
|
96
96
|
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
data/bin/brainiac
CHANGED
|
@@ -1395,6 +1395,307 @@ when "provider"
|
|
|
1395
1395
|
HELP
|
|
1396
1396
|
end
|
|
1397
1397
|
|
|
1398
|
+
when "agent"
|
|
1399
|
+
agent_cmd = ARGV.shift
|
|
1400
|
+
agent_registry_file = File.join(BRAINIAC_DIR, "agents.json")
|
|
1401
|
+
|
|
1402
|
+
case agent_cmd
|
|
1403
|
+
when "list", "ls"
|
|
1404
|
+
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
1405
|
+
if registry.empty?
|
|
1406
|
+
puts "No agents in registry."
|
|
1407
|
+
else
|
|
1408
|
+
agents = registry.select { |_, e| e.is_a?(Hash) }.map do |key, entry|
|
|
1409
|
+
display = entry["fizzy_name"] || key.capitalize
|
|
1410
|
+
details = []
|
|
1411
|
+
if entry["role"]
|
|
1412
|
+
roles = Array(entry["role"])
|
|
1413
|
+
details << roles.join(", ")
|
|
1414
|
+
end
|
|
1415
|
+
details << "cli:#{entry["cli_provider"]}" if entry["cli_provider"]
|
|
1416
|
+
details << "env:#{entry["env"].size}" if entry["env"]&.any?
|
|
1417
|
+
{ key: key, display: display, local: entry["local"], details: details.join(" | ") }
|
|
1418
|
+
end
|
|
1419
|
+
|
|
1420
|
+
local, remote = agents.partition { |a| a[:local] }
|
|
1421
|
+
|
|
1422
|
+
local.each do |a|
|
|
1423
|
+
line = " #{a[:display]} (#{a[:key]})"
|
|
1424
|
+
line += " #{a[:details]}" unless a[:details].empty?
|
|
1425
|
+
puts line
|
|
1426
|
+
end
|
|
1427
|
+
|
|
1428
|
+
unless remote.empty?
|
|
1429
|
+
puts "" if local.any?
|
|
1430
|
+
puts " Remote:" if local.any?
|
|
1431
|
+
remote.each do |a|
|
|
1432
|
+
line = " #{a[:display]} (#{a[:key]})"
|
|
1433
|
+
line += " #{a[:details]}" unless a[:details].empty?
|
|
1434
|
+
puts line
|
|
1435
|
+
end
|
|
1436
|
+
end
|
|
1437
|
+
end
|
|
1438
|
+
|
|
1439
|
+
when nil
|
|
1440
|
+
puts <<~HELP
|
|
1441
|
+
Usage: brainiac agent <name> <command> [args]
|
|
1442
|
+
brainiac agent list
|
|
1443
|
+
|
|
1444
|
+
Commands:
|
|
1445
|
+
list List all agents in the registry
|
|
1446
|
+
<name> show Show agent configuration (tokens redacted)
|
|
1447
|
+
<name> env <KEY> <VALUE> Set an env var for an agent
|
|
1448
|
+
<name> env List env vars for an agent
|
|
1449
|
+
<name> env --delete <KEY> Remove an env var from an agent
|
|
1450
|
+
|
|
1451
|
+
Examples:
|
|
1452
|
+
brainiac agent galen env FIZZY_TOKEN fizzy_abc123
|
|
1453
|
+
brainiac agent galen env DISCORD_BOT_TOKEN Bot_xyz789
|
|
1454
|
+
brainiac agent galen env
|
|
1455
|
+
brainiac agent galen env --delete DISCORD_BOT_TOKEN
|
|
1456
|
+
brainiac agent galen show
|
|
1457
|
+
HELP
|
|
1458
|
+
|
|
1459
|
+
else
|
|
1460
|
+
# Pattern: brainiac agent <name> <subcommand> [args]
|
|
1461
|
+
agent_key = agent_cmd.downcase.gsub(/[^a-z0-9-]/, "-")
|
|
1462
|
+
sub = ARGV.shift
|
|
1463
|
+
|
|
1464
|
+
case sub
|
|
1465
|
+
when "show", nil
|
|
1466
|
+
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
1467
|
+
entry = registry[agent_key]
|
|
1468
|
+
unless entry
|
|
1469
|
+
puts "Agent '#{agent_cmd}' not found in registry."
|
|
1470
|
+
exit 1
|
|
1471
|
+
end
|
|
1472
|
+
# Redact tokens in output
|
|
1473
|
+
display = entry.dup
|
|
1474
|
+
if display["env"]
|
|
1475
|
+
display["env"] = display["env"].transform_values { |v| v.length > 10 ? "#{v[0..6]}...#{v[-4..]}" : v }
|
|
1476
|
+
end
|
|
1477
|
+
puts JSON.pretty_generate(display)
|
|
1478
|
+
|
|
1479
|
+
when "env"
|
|
1480
|
+
if ARGV[0] == "--delete"
|
|
1481
|
+
ARGV.shift
|
|
1482
|
+
var_name = ARGV.shift
|
|
1483
|
+
unless var_name
|
|
1484
|
+
puts "Usage: brainiac agent #{agent_cmd} env --delete <KEY>"
|
|
1485
|
+
exit 1
|
|
1486
|
+
end
|
|
1487
|
+
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
1488
|
+
unless registry.dig(agent_key, "env", var_name)
|
|
1489
|
+
puts "#{var_name} not set for '#{agent_cmd}'."
|
|
1490
|
+
exit 1
|
|
1491
|
+
end
|
|
1492
|
+
registry[agent_key]["env"].delete(var_name)
|
|
1493
|
+
registry[agent_key].delete("env") if registry[agent_key]["env"].empty?
|
|
1494
|
+
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
1495
|
+
puts "✓ Removed #{var_name} from #{agent_cmd}"
|
|
1496
|
+
|
|
1497
|
+
elsif ARGV.empty?
|
|
1498
|
+
# List env vars
|
|
1499
|
+
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
1500
|
+
entry = registry[agent_key]
|
|
1501
|
+
unless entry.is_a?(Hash) && entry["env"]&.any?
|
|
1502
|
+
puts "No env vars configured for '#{agent_cmd}'."
|
|
1503
|
+
exit 0
|
|
1504
|
+
end
|
|
1505
|
+
entry["env"].each do |k, v|
|
|
1506
|
+
preview = v.length > 20 ? "#{v[0..16]}..." : v
|
|
1507
|
+
puts " #{k}=#{preview}"
|
|
1508
|
+
end
|
|
1509
|
+
|
|
1510
|
+
else
|
|
1511
|
+
var_name = ARGV.shift
|
|
1512
|
+
var_value = ARGV.shift
|
|
1513
|
+
unless var_name && var_value
|
|
1514
|
+
puts "Usage: brainiac agent #{agent_cmd} env <KEY> <VALUE>"
|
|
1515
|
+
exit 1
|
|
1516
|
+
end
|
|
1517
|
+
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
1518
|
+
registry[agent_key] ||= {}
|
|
1519
|
+
registry[agent_key]["env"] ||= {}
|
|
1520
|
+
registry[agent_key]["env"][var_name] = var_value
|
|
1521
|
+
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
1522
|
+
puts "✓ Set #{var_name} for #{agent_cmd}"
|
|
1523
|
+
end
|
|
1524
|
+
|
|
1525
|
+
else
|
|
1526
|
+
puts "Unknown subcommand '#{sub}' for agent '#{agent_cmd}'."
|
|
1527
|
+
puts "Available: show, env"
|
|
1528
|
+
exit 1
|
|
1529
|
+
end
|
|
1530
|
+
end
|
|
1531
|
+
|
|
1532
|
+
when "role"
|
|
1533
|
+
role_cmd = ARGV.shift
|
|
1534
|
+
roles_dir = File.join(BRAINIAC_DIR, "roles")
|
|
1535
|
+
|
|
1536
|
+
case role_cmd
|
|
1537
|
+
when "list", "ls"
|
|
1538
|
+
unless Dir.exist?(roles_dir)
|
|
1539
|
+
puts "No roles configured. Create markdown files in #{roles_dir}/"
|
|
1540
|
+
exit 0
|
|
1541
|
+
end
|
|
1542
|
+
files = Dir.glob(File.join(roles_dir, "*.md"))
|
|
1543
|
+
if files.empty?
|
|
1544
|
+
puts "No roles configured. Create markdown files in #{roles_dir}/"
|
|
1545
|
+
else
|
|
1546
|
+
# Show which agents use each role
|
|
1547
|
+
registry_file = File.join(BRAINIAC_DIR, "agents.json")
|
|
1548
|
+
registry = File.exist?(registry_file) ? JSON.parse(File.read(registry_file)) : {}
|
|
1549
|
+
agent_roles = {}
|
|
1550
|
+
registry.each do |key, entry|
|
|
1551
|
+
next unless entry.is_a?(Hash) && entry["role"]
|
|
1552
|
+
|
|
1553
|
+
roles = entry["role"].is_a?(Array) ? entry["role"] : [entry["role"]]
|
|
1554
|
+
roles.each do |r|
|
|
1555
|
+
agent_roles[r] ||= []
|
|
1556
|
+
agent_roles[r] << (entry["fizzy_name"] || key.capitalize)
|
|
1557
|
+
end
|
|
1558
|
+
end
|
|
1559
|
+
|
|
1560
|
+
files.each do |f|
|
|
1561
|
+
name = File.basename(f, ".md")
|
|
1562
|
+
# Read first non-empty, non-heading line as description
|
|
1563
|
+
desc = File.readlines(f).find { |l| !l.strip.empty? && !l.start_with?("#") && !l.start_with?("---") }&.strip || ""
|
|
1564
|
+
agents = agent_roles[name]
|
|
1565
|
+
agent_str = agents ? " (#{agents.join(", ")})" : ""
|
|
1566
|
+
puts "#{name}#{agent_str} — #{desc[0..70]}"
|
|
1567
|
+
end
|
|
1568
|
+
end
|
|
1569
|
+
|
|
1570
|
+
when "show"
|
|
1571
|
+
name = ARGV[0]
|
|
1572
|
+
unless name
|
|
1573
|
+
puts "Usage: brainiac role show <name>"
|
|
1574
|
+
exit 1
|
|
1575
|
+
end
|
|
1576
|
+
file = File.join(roles_dir, "#{name}.md")
|
|
1577
|
+
unless File.exist?(file)
|
|
1578
|
+
puts "Role '#{name}' not found. Available: #{Dir.glob(File.join(roles_dir, "*.md")).map { |f| File.basename(f, ".md") }.join(", ")}"
|
|
1579
|
+
exit 1
|
|
1580
|
+
end
|
|
1581
|
+
puts File.read(file)
|
|
1582
|
+
|
|
1583
|
+
when "assign"
|
|
1584
|
+
agent_name = ARGV[0]
|
|
1585
|
+
role_name = ARGV[1]
|
|
1586
|
+
unless agent_name && role_name
|
|
1587
|
+
puts "Usage: brainiac role assign <agent> <role>"
|
|
1588
|
+
exit 1
|
|
1589
|
+
end
|
|
1590
|
+
|
|
1591
|
+
file = File.join(roles_dir, "#{role_name}.md")
|
|
1592
|
+
unless File.exist?(file)
|
|
1593
|
+
puts "Role '#{role_name}' not found."
|
|
1594
|
+
exit 1
|
|
1595
|
+
end
|
|
1596
|
+
|
|
1597
|
+
registry_file = File.join(BRAINIAC_DIR, "agents.json")
|
|
1598
|
+
registry = File.exist?(registry_file) ? JSON.parse(File.read(registry_file)) : {}
|
|
1599
|
+
key = agent_name.downcase.gsub(/[^a-z0-9-]/, "-")
|
|
1600
|
+
registry[key] ||= {}
|
|
1601
|
+
|
|
1602
|
+
# Normalize existing role field to array
|
|
1603
|
+
existing = registry[key]["role"]
|
|
1604
|
+
roles = case existing
|
|
1605
|
+
when Array then existing
|
|
1606
|
+
when String then [existing]
|
|
1607
|
+
else []
|
|
1608
|
+
end
|
|
1609
|
+
|
|
1610
|
+
if roles.include?(role_name)
|
|
1611
|
+
puts "#{agent_name} already has role '#{role_name}'"
|
|
1612
|
+
else
|
|
1613
|
+
roles << role_name
|
|
1614
|
+
registry[key]["role"] = roles.size == 1 ? roles.first : roles
|
|
1615
|
+
File.write(registry_file, JSON.pretty_generate(registry))
|
|
1616
|
+
puts "✓ Assigned role '#{role_name}' to #{agent_name}"
|
|
1617
|
+
end
|
|
1618
|
+
|
|
1619
|
+
when "unassign", "remove-from"
|
|
1620
|
+
agent_name = ARGV[0]
|
|
1621
|
+
role_name = ARGV[1]
|
|
1622
|
+
unless agent_name && role_name
|
|
1623
|
+
puts "Usage: brainiac role unassign <agent> <role>"
|
|
1624
|
+
exit 1
|
|
1625
|
+
end
|
|
1626
|
+
|
|
1627
|
+
registry_file = File.join(BRAINIAC_DIR, "agents.json")
|
|
1628
|
+
registry = File.exist?(registry_file) ? JSON.parse(File.read(registry_file)) : {}
|
|
1629
|
+
key = agent_name.downcase.gsub(/[^a-z0-9-]/, "-")
|
|
1630
|
+
|
|
1631
|
+
unless registry[key]
|
|
1632
|
+
puts "Agent '#{agent_name}' not found in registry."
|
|
1633
|
+
exit 1
|
|
1634
|
+
end
|
|
1635
|
+
|
|
1636
|
+
existing = registry[key]["role"]
|
|
1637
|
+
roles = case existing
|
|
1638
|
+
when Array then existing
|
|
1639
|
+
when String then [existing]
|
|
1640
|
+
else []
|
|
1641
|
+
end
|
|
1642
|
+
|
|
1643
|
+
unless roles.include?(role_name)
|
|
1644
|
+
puts "#{agent_name} doesn't have role '#{role_name}'"
|
|
1645
|
+
exit 1
|
|
1646
|
+
end
|
|
1647
|
+
|
|
1648
|
+
roles.delete(role_name)
|
|
1649
|
+
if roles.empty?
|
|
1650
|
+
registry[key].delete("role")
|
|
1651
|
+
elsif roles.size == 1
|
|
1652
|
+
registry[key]["role"] = roles.first
|
|
1653
|
+
else
|
|
1654
|
+
registry[key]["role"] = roles
|
|
1655
|
+
end
|
|
1656
|
+
File.write(registry_file, JSON.pretty_generate(registry))
|
|
1657
|
+
puts "✓ Removed role '#{role_name}' from #{agent_name}"
|
|
1658
|
+
|
|
1659
|
+
when "create", "add"
|
|
1660
|
+
name = ARGV[0]
|
|
1661
|
+
unless name
|
|
1662
|
+
puts "Usage: brainiac role create <name>"
|
|
1663
|
+
exit 1
|
|
1664
|
+
end
|
|
1665
|
+
FileUtils.mkdir_p(roles_dir)
|
|
1666
|
+
file = File.join(roles_dir, "#{name}.md")
|
|
1667
|
+
if File.exist?(file)
|
|
1668
|
+
puts "Role '#{name}' already exists. Edit #{file} directly."
|
|
1669
|
+
exit 1
|
|
1670
|
+
end
|
|
1671
|
+
File.write(file, "# Role: #{name.split("-").map(&:capitalize).join(" ")}\n\nDescribe this role's responsibilities here.\n")
|
|
1672
|
+
puts "Created #{file} — edit it to define the role."
|
|
1673
|
+
|
|
1674
|
+
else
|
|
1675
|
+
puts <<~HELP
|
|
1676
|
+
Usage: brainiac role <command>
|
|
1677
|
+
|
|
1678
|
+
Commands:
|
|
1679
|
+
list List configured roles
|
|
1680
|
+
show <name> Show role definition
|
|
1681
|
+
create <name> Create a new role file
|
|
1682
|
+
assign <agent> <role> Assign a role to an agent (additive)
|
|
1683
|
+
unassign <agent> <role> Remove a role from an agent
|
|
1684
|
+
HELP
|
|
1685
|
+
end
|
|
1686
|
+
|
|
1687
|
+
when "completions"
|
|
1688
|
+
completion_file = File.join(__dir__, "brainiac-completion.bash")
|
|
1689
|
+
if ARGV[0] == "--install"
|
|
1690
|
+
target = File.expand_path("~/.brainiac/brainiac-completion.bash")
|
|
1691
|
+
FileUtils.cp(completion_file, target)
|
|
1692
|
+
puts "Installed to #{target}"
|
|
1693
|
+
puts "Add to your ~/.bashrc:"
|
|
1694
|
+
puts " source #{target}"
|
|
1695
|
+
else
|
|
1696
|
+
puts File.read(completion_file)
|
|
1697
|
+
end
|
|
1698
|
+
|
|
1398
1699
|
when "version", "--version", "-v"
|
|
1399
1700
|
puts "brainiac #{BRAINIAC_VERSION}"
|
|
1400
1701
|
|
|
@@ -1419,6 +1720,8 @@ when "help", "--help", "-h", nil
|
|
|
1419
1720
|
brainiac discord <command> Manage the Discord bot
|
|
1420
1721
|
brainiac cron <command> Manage scheduled agent tasks
|
|
1421
1722
|
brainiac provider <command> Manage CLI providers
|
|
1723
|
+
brainiac role <command> Manage agent roles
|
|
1724
|
+
brainiac agent <command> Manage agent registry (env, list, show)
|
|
1422
1725
|
brainiac config Configure Brainiac CLI
|
|
1423
1726
|
brainiac path Show Brainiac config directory
|
|
1424
1727
|
brainiac version Show version
|
|
@@ -1462,6 +1765,20 @@ when "help", "--help", "-h", nil
|
|
|
1462
1765
|
provider show <name> Show provider configuration
|
|
1463
1766
|
provider add <name> Create a new provider config
|
|
1464
1767
|
|
|
1768
|
+
Role Commands:
|
|
1769
|
+
role list List configured roles and assignments
|
|
1770
|
+
role show <name> Show role definition
|
|
1771
|
+
role create <name> Create a new role file
|
|
1772
|
+
role assign <agent> <role> Assign a role to an agent (additive)
|
|
1773
|
+
role unassign <agent> <role> Remove a role from an agent
|
|
1774
|
+
|
|
1775
|
+
Agent Commands:
|
|
1776
|
+
agent list List all agents in the registry
|
|
1777
|
+
agent <name> show Show agent configuration (tokens redacted)
|
|
1778
|
+
agent <name> env <KEY> <VALUE> Set an env var for an agent
|
|
1779
|
+
agent <name> env List env vars for an agent
|
|
1780
|
+
agent <name> env --delete <KEY> Remove an env var from an agent
|
|
1781
|
+
|
|
1465
1782
|
Examples:
|
|
1466
1783
|
# Start the server in the foreground (like rails server)
|
|
1467
1784
|
brainiac server
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
_brainiac() {
|
|
2
|
+
local cur prev words cword
|
|
3
|
+
COMPREPLY=()
|
|
4
|
+
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
5
|
+
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
6
|
+
words=("${COMP_WORDS[@]}")
|
|
7
|
+
cword=$COMP_CWORD
|
|
8
|
+
|
|
9
|
+
local brainiac_dir="${BRAINIAC_DIR:-$HOME/.brainiac}"
|
|
10
|
+
|
|
11
|
+
# Top-level commands
|
|
12
|
+
local commands="server stop restart logs status register unregister list show brain discord cron provider role agent config path version help setup projects card-map"
|
|
13
|
+
|
|
14
|
+
# Helper: list agent keys from registry
|
|
15
|
+
_brainiac_agents() {
|
|
16
|
+
if [[ -f "$brainiac_dir/agents.json" ]]; then
|
|
17
|
+
ruby -rjson -e 'JSON.parse(File.read(ARGV[0])).each_key { |k| puts k }' "$brainiac_dir/agents.json" 2>/dev/null
|
|
18
|
+
fi
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Helper: list role names from roles directory
|
|
22
|
+
_brainiac_roles() {
|
|
23
|
+
if [[ -d "$brainiac_dir/roles" ]]; then
|
|
24
|
+
ls "$brainiac_dir/roles/"*.md 2>/dev/null | xargs -I{} basename {} .md
|
|
25
|
+
fi
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Helper: list provider names
|
|
29
|
+
_brainiac_providers() {
|
|
30
|
+
if [[ -d "$brainiac_dir/cli-providers" ]]; then
|
|
31
|
+
ls "$brainiac_dir/cli-providers/"*.json 2>/dev/null | xargs -I{} basename {} .json
|
|
32
|
+
fi
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Determine position in command
|
|
36
|
+
case $cword in
|
|
37
|
+
1)
|
|
38
|
+
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
39
|
+
return
|
|
40
|
+
;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
local cmd="${words[1]}"
|
|
44
|
+
|
|
45
|
+
case "$cmd" in
|
|
46
|
+
role)
|
|
47
|
+
case $cword in
|
|
48
|
+
2)
|
|
49
|
+
COMPREPLY=($(compgen -W "list show create assign unassign" -- "$cur"))
|
|
50
|
+
;;
|
|
51
|
+
3)
|
|
52
|
+
local subcmd="${words[2]}"
|
|
53
|
+
case "$subcmd" in
|
|
54
|
+
assign|unassign)
|
|
55
|
+
COMPREPLY=($(compgen -W "$(_brainiac_agents)" -- "$cur"))
|
|
56
|
+
;;
|
|
57
|
+
show)
|
|
58
|
+
COMPREPLY=($(compgen -W "$(_brainiac_roles)" -- "$cur"))
|
|
59
|
+
;;
|
|
60
|
+
esac
|
|
61
|
+
;;
|
|
62
|
+
4)
|
|
63
|
+
local subcmd="${words[2]}"
|
|
64
|
+
case "$subcmd" in
|
|
65
|
+
assign|unassign)
|
|
66
|
+
COMPREPLY=($(compgen -W "$(_brainiac_roles)" -- "$cur"))
|
|
67
|
+
;;
|
|
68
|
+
esac
|
|
69
|
+
;;
|
|
70
|
+
esac
|
|
71
|
+
;;
|
|
72
|
+
|
|
73
|
+
agent)
|
|
74
|
+
case $cword in
|
|
75
|
+
2)
|
|
76
|
+
COMPREPLY=($(compgen -W "list $(_brainiac_agents)" -- "$cur"))
|
|
77
|
+
;;
|
|
78
|
+
3)
|
|
79
|
+
local agent_name="${words[2]}"
|
|
80
|
+
if [[ "$agent_name" != "list" ]]; then
|
|
81
|
+
COMPREPLY=($(compgen -W "show env" -- "$cur"))
|
|
82
|
+
fi
|
|
83
|
+
;;
|
|
84
|
+
4)
|
|
85
|
+
local subcmd="${words[3]}"
|
|
86
|
+
if [[ "$subcmd" == "env" ]]; then
|
|
87
|
+
# Suggest --delete or existing env var names for this agent
|
|
88
|
+
local agent_key="${words[2]}"
|
|
89
|
+
local env_keys=""
|
|
90
|
+
if [[ -f "$brainiac_dir/agents.json" ]]; then
|
|
91
|
+
env_keys=$(ruby -rjson -e '
|
|
92
|
+
reg = JSON.parse(File.read(ARGV[0]))
|
|
93
|
+
entry = reg[ARGV[1]] || reg[ARGV[1].downcase]
|
|
94
|
+
(entry&.dig("env") || {}).each_key { |k| puts k }
|
|
95
|
+
' "$brainiac_dir/agents.json" "$agent_key" 2>/dev/null)
|
|
96
|
+
fi
|
|
97
|
+
COMPREPLY=($(compgen -W "--delete $env_keys" -- "$cur"))
|
|
98
|
+
fi
|
|
99
|
+
;;
|
|
100
|
+
5)
|
|
101
|
+
# After --delete, suggest env var names
|
|
102
|
+
if [[ "${words[3]}" == "env" && "${words[4]}" == "--delete" ]]; then
|
|
103
|
+
local agent_key="${words[2]}"
|
|
104
|
+
local env_keys=""
|
|
105
|
+
if [[ -f "$brainiac_dir/agents.json" ]]; then
|
|
106
|
+
env_keys=$(ruby -rjson -e '
|
|
107
|
+
reg = JSON.parse(File.read(ARGV[0]))
|
|
108
|
+
entry = reg[ARGV[1]] || reg[ARGV[1].downcase]
|
|
109
|
+
(entry&.dig("env") || {}).each_key { |k| puts k }
|
|
110
|
+
' "$brainiac_dir/agents.json" "$agent_key" 2>/dev/null)
|
|
111
|
+
fi
|
|
112
|
+
COMPREPLY=($(compgen -W "$env_keys" -- "$cur"))
|
|
113
|
+
fi
|
|
114
|
+
;;
|
|
115
|
+
esac
|
|
116
|
+
;;
|
|
117
|
+
|
|
118
|
+
provider)
|
|
119
|
+
case $cword in
|
|
120
|
+
2)
|
|
121
|
+
COMPREPLY=($(compgen -W "list show add" -- "$cur"))
|
|
122
|
+
;;
|
|
123
|
+
3)
|
|
124
|
+
local subcmd="${words[2]}"
|
|
125
|
+
if [[ "$subcmd" == "show" ]]; then
|
|
126
|
+
COMPREPLY=($(compgen -W "$(_brainiac_providers)" -- "$cur"))
|
|
127
|
+
fi
|
|
128
|
+
;;
|
|
129
|
+
esac
|
|
130
|
+
;;
|
|
131
|
+
|
|
132
|
+
brain)
|
|
133
|
+
case $cword in
|
|
134
|
+
2)
|
|
135
|
+
COMPREPLY=($(compgen -W "init status search list path" -- "$cur"))
|
|
136
|
+
;;
|
|
137
|
+
3)
|
|
138
|
+
local subcmd="${words[2]}"
|
|
139
|
+
if [[ "$subcmd" == "init" || "$subcmd" == "status" ]]; then
|
|
140
|
+
COMPREPLY=($(compgen -W "$(_brainiac_agents)" -- "$cur"))
|
|
141
|
+
fi
|
|
142
|
+
;;
|
|
143
|
+
esac
|
|
144
|
+
;;
|
|
145
|
+
|
|
146
|
+
discord)
|
|
147
|
+
case $cword in
|
|
148
|
+
2)
|
|
149
|
+
COMPREPLY=($(compgen -W "config default map owner token agents status" -- "$cur"))
|
|
150
|
+
;;
|
|
151
|
+
3)
|
|
152
|
+
local subcmd="${words[2]}"
|
|
153
|
+
if [[ "$subcmd" == "token" ]]; then
|
|
154
|
+
COMPREPLY=($(compgen -W "$(_brainiac_agents)" -- "$cur"))
|
|
155
|
+
fi
|
|
156
|
+
;;
|
|
157
|
+
esac
|
|
158
|
+
;;
|
|
159
|
+
|
|
160
|
+
cron)
|
|
161
|
+
case $cword in
|
|
162
|
+
2)
|
|
163
|
+
COMPREPLY=($(compgen -W "add list remove enable disable update" -- "$cur"))
|
|
164
|
+
;;
|
|
165
|
+
esac
|
|
166
|
+
;;
|
|
167
|
+
|
|
168
|
+
projects)
|
|
169
|
+
case $cword in
|
|
170
|
+
2)
|
|
171
|
+
COMPREPLY=($(compgen -W "list default" -- "$cur"))
|
|
172
|
+
;;
|
|
173
|
+
esac
|
|
174
|
+
;;
|
|
175
|
+
esac
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
complete -F _brainiac brainiac
|
data/lib/brainiac/agents.rb
CHANGED
|
@@ -119,6 +119,40 @@ def fizzy_display_name(agent_name)
|
|
|
119
119
|
entry["fizzy_name"] || agent_name
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
+
# Get the role name(s) configured for an agent in agents.json.
|
|
123
|
+
# Returns an array of role names (may be empty).
|
|
124
|
+
def agent_roles_for(agent_name)
|
|
125
|
+
return [] unless agent_name
|
|
126
|
+
|
|
127
|
+
key = agent_name.downcase.gsub(/[^a-z0-9-]/, "-")
|
|
128
|
+
entry = AGENT_REGISTRY[key]
|
|
129
|
+
return [] unless entry.is_a?(Hash)
|
|
130
|
+
|
|
131
|
+
roles = entry["role"]
|
|
132
|
+
case roles
|
|
133
|
+
when Array then roles
|
|
134
|
+
when String then [roles]
|
|
135
|
+
else []
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Load a role definition from ~/.brainiac/roles/<name>.md.
|
|
140
|
+
# Returns the file content (markdown) or nil if not found.
|
|
141
|
+
def load_role(role_name)
|
|
142
|
+
return nil unless role_name
|
|
143
|
+
|
|
144
|
+
role_file = File.join(ROLES_DIR, "#{role_name}.md")
|
|
145
|
+
return nil unless File.exist?(role_file)
|
|
146
|
+
|
|
147
|
+
content = File.read(role_file).strip
|
|
148
|
+
# Strip YAML front matter if present
|
|
149
|
+
content = content.sub(/\A---\n.*?\n---\n*/m, "").strip
|
|
150
|
+
content.empty? ? nil : content
|
|
151
|
+
rescue StandardError => e
|
|
152
|
+
LOG.warn "Failed to load role '#{role_name}': #{e.message}"
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
122
156
|
def agent_roster
|
|
123
157
|
roster = {}
|
|
124
158
|
all_agent_names.each { |name| roster[name.downcase] = fizzy_display_name(name) }
|
data/lib/brainiac/brain.rb
CHANGED
|
@@ -134,6 +134,27 @@ def extract_topics(card_title, comment_body, project_key)
|
|
|
134
134
|
topics.compact.uniq
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
+
def build_role_section(agent_name)
|
|
138
|
+
roles = agent_roles_for(agent_name)
|
|
139
|
+
return nil if roles.empty?
|
|
140
|
+
|
|
141
|
+
sections = roles.filter_map do |role_name|
|
|
142
|
+
content = load_role(role_name)
|
|
143
|
+
next unless content
|
|
144
|
+
|
|
145
|
+
"### #{role_name}\n\n#{content}"
|
|
146
|
+
end
|
|
147
|
+
return nil if sections.empty?
|
|
148
|
+
|
|
149
|
+
<<~ROLE
|
|
150
|
+
## Role#{"s" if roles.size > 1} (#{roles.join(", ")})
|
|
151
|
+
The following defines your role and responsibilities for this session.
|
|
152
|
+
Follow these instructions for how you approach work.
|
|
153
|
+
|
|
154
|
+
#{sections.join("\n\n")}
|
|
155
|
+
ROLE
|
|
156
|
+
end
|
|
157
|
+
|
|
137
158
|
def build_brain_context(agent_name: AI_AGENT_NAME, card_title: "", card_number: nil, project_key: nil, comment_body: "", source: nil)
|
|
138
159
|
Thread.new { brain_pull }
|
|
139
160
|
|
|
@@ -164,6 +185,10 @@ def build_brain_context(agent_name: AI_AGENT_NAME, card_title: "", card_number:
|
|
|
164
185
|
|
|
165
186
|
sections = []
|
|
166
187
|
|
|
188
|
+
# Role: CLI-agnostic agent role definition from ~/.brainiac/roles/
|
|
189
|
+
role_section = build_role_section(agent_name)
|
|
190
|
+
sections << role_section if role_section
|
|
191
|
+
|
|
167
192
|
unless persona_result.empty?
|
|
168
193
|
sections << <<~PERSONA
|
|
169
194
|
## Brain — Persona (auto-retrieved, CRITICAL)
|
data/lib/brainiac/config.rb
CHANGED
|
@@ -49,6 +49,10 @@ MEMORY_BASE_DIR = File.join(BRAINIAC_DIR, "brain", "memory")
|
|
|
49
49
|
MEMORY_FILE_TEMPLATE = "card-{{CARD_ID}}.md"
|
|
50
50
|
KNOWLEDGE_COLLECTION = "brainiac-knowledge"
|
|
51
51
|
|
|
52
|
+
# --- Roles ---
|
|
53
|
+
|
|
54
|
+
ROLES_DIR = File.join(BRAINIAC_DIR, "roles")
|
|
55
|
+
|
|
52
56
|
# --- Fizzy auth ---
|
|
53
57
|
|
|
54
58
|
FIZZY_CONFIG_FILE = File.join(BRAINIAC_DIR, "fizzy.json")
|
data/lib/brainiac/cron.rb
CHANGED
|
@@ -542,7 +542,9 @@ def execute_cron_job(job)
|
|
|
542
542
|
FileUtils.mkdir_p(File.dirname(log_file))
|
|
543
543
|
|
|
544
544
|
prompt_file = write_cron_prompt_file(job, prompt_data[:full_prompt], timestamp)
|
|
545
|
-
|
|
545
|
+
resolved = resolve_project_cli_config(project, agent_name: agent_name)
|
|
546
|
+
cmd = build_cron_agent_cmd(job, project, prompt_file: prompt_file)
|
|
547
|
+
prompt_mode = resolved["prompt_mode"] || "stdin"
|
|
546
548
|
|
|
547
549
|
LOG.info "[Cron] Dispatching job #{job[:id]} with #{agent_name}, tail -f #{log_file}"
|
|
548
550
|
|
|
@@ -551,7 +553,7 @@ def execute_cron_job(job)
|
|
|
551
553
|
|
|
552
554
|
pid = spawn(spawn_env, *cmd,
|
|
553
555
|
chdir: project["repo_path"],
|
|
554
|
-
in: prompt_file,
|
|
556
|
+
**(prompt_mode == "stdin" ? { in: prompt_file } : {}),
|
|
555
557
|
out: [log_file, "w"],
|
|
556
558
|
err: %i[child out])
|
|
557
559
|
|
|
@@ -573,15 +575,16 @@ def write_cron_prompt_file(job, prompt_content, timestamp)
|
|
|
573
575
|
end
|
|
574
576
|
|
|
575
577
|
# Build the CLI command array for a cron agent invocation.
|
|
576
|
-
def build_cron_agent_cmd(job, project)
|
|
578
|
+
def build_cron_agent_cmd(job, project, prompt_file: nil)
|
|
577
579
|
agent_config_name = job[:agent].downcase.gsub(/[^a-z0-9-]/, "-")
|
|
578
|
-
resolved = resolve_project_cli_config(project)
|
|
580
|
+
resolved = resolve_project_cli_config(project, agent_name: job[:agent])
|
|
581
|
+
agent_flag = resolved.key?("agent_flag") ? resolved["agent_flag"] : "--agent"
|
|
579
582
|
cmd = [resolved["agent_cli"]]
|
|
580
|
-
cmd.push(
|
|
583
|
+
cmd.push(agent_flag, agent_config_name) if agent_flag
|
|
581
584
|
cmd.concat(resolved["agent_cli_args"].split)
|
|
582
|
-
add_trust_tools!(cmd, resolved["agent_cli_args"])
|
|
583
585
|
cmd.push(resolved["agent_model_flag"], job[:model]) if resolved["agent_model_flag"]&.length&.positive? && job[:model]
|
|
584
586
|
cmd.push(resolved["agent_effort_flag"], job[:effort]) if resolved["agent_effort_flag"]&.length&.positive? && job[:effort]
|
|
587
|
+
cmd.push(resolved["prompt_flag"], prompt_file) if prompt_file && resolved["prompt_mode"] == "flag" && resolved["prompt_flag"]
|
|
585
588
|
cmd
|
|
586
589
|
end
|
|
587
590
|
|