brainiac 0.0.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6381c1bf1bde343e16c3eef756bb6a761b3640e1adfc645ab3e7deb202c11d80
4
- data.tar.gz: c2391c9fcc3794a03a18bafcf12c6610ca354069b7d7a98aa0a5e5087eff5589
3
+ metadata.gz: 1ecf3d562c59eaeb76f9c1e4ceb87f3ed8c1cabb4cc6c3ddc6837e7c38fe4e8d
4
+ data.tar.gz: 60abd844fa6877850aaf37f603229beae693d3aa8b8d82194ac378844009e766
5
5
  SHA512:
6
- metadata.gz: 6edc692e8d96466267b99044787c4150441d8970eb004aee050343ec205e717756d8cd7e148ba886590ca3634cc4c6b831802037f9d4e946ebd30fadae17c0c6
7
- data.tar.gz: c2f136d341a372039ec5d45b775488b9efc3f97d01679d3ce7f4765e0e397d45e54fa83cc698068be98e38fec8cc2fe4e778425d2d712ea8fa1046787822ef09
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.2)
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.2)
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
@@ -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) }
@@ -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)
@@ -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
- cmd = build_cron_agent_cmd(job, project)
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("--agent", agent_config_name)
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