brainiac 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +63 -0
- data/lib/brainiac/helpers.rb +143 -9
- data/lib/brainiac/profiles.rb +46 -6
- data/lib/brainiac/session_history.rb +1 -0
- data/lib/brainiac/version.rb +1 -1
- data/templates/profiles.json.example +7 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d73484edd8f42558e933347b45b50f5aa27065a86798ae4d764e44b0d07534f9
|
|
4
|
+
data.tar.gz: bcc9a1a5bf6094ef8138cf345e2e517004c412d983c4475c80b89cb005bbf6aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ecd465ba660b6a145c15a9fa7fd17a13dad35bf2dff802b18dcba92949964b5df0be02e26ebc643a74582f010cecf520fc7d04cd32b4acda83891768cd0c6c1
|
|
7
|
+
data.tar.gz: ca830bd810461ff79c474bf5a2744f187a29ee8040d7af0b14c7143dd76c92dbb6e1117f942e51610ecb8e3bdb9d48739a48f7862006f8f84a4d7a66bb40481a
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
brainiac (0.1.
|
|
4
|
+
brainiac (0.1.4)
|
|
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.1.
|
|
87
|
+
brainiac (0.1.4)
|
|
88
88
|
json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
|
|
89
89
|
language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
|
|
90
90
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
data/README.md
CHANGED
|
@@ -661,6 +661,69 @@ brainiac discord config # Show current Discord config
|
|
|
661
661
|
brainiac discord status # Check bot status via server API
|
|
662
662
|
```
|
|
663
663
|
|
|
664
|
+
## Profiles (Multi-Account)
|
|
665
|
+
|
|
666
|
+
Profiles let you inject a named set of environment variables into an agent dispatch without standing up a separate agent. The canonical use-case is running different CLI accounts (e.g. two AWS accounts) from the same brainiac instance.
|
|
667
|
+
|
|
668
|
+
### How it works
|
|
669
|
+
|
|
670
|
+
Add a `[profile:name]` or `[p:name]` inline tag to any Discord message, Fizzy comment, or GitHub message. The matching profile's env is merged into the dispatch — same CLI, same agent, different credentials.
|
|
671
|
+
|
|
672
|
+
### kiro-cli: two env vars, two concerns
|
|
673
|
+
|
|
674
|
+
kiro-cli splits state across two independent dirs:
|
|
675
|
+
|
|
676
|
+
| Env var | What it controls | File |
|
|
677
|
+
|---------|-----------------|------|
|
|
678
|
+
| `XDG_DATA_HOME` | Auth store — tokens + which account you're logged into | `kiro-cli/data.sqlite3` |
|
|
679
|
+
| `KIRO_HOME` | Settings — `chat.defaultModel`, agents, MCP, permissions, steering | `settings/cli.json` |
|
|
680
|
+
|
|
681
|
+
Give each profile its own `KIRO_HOME` and each account gets its own `chat.defaultModel`. That's the clean way to run different accounts on different models — set each account's model once and let it drive.
|
|
682
|
+
|
|
683
|
+
### Configuration (`~/.brainiac/profiles.json`)
|
|
684
|
+
|
|
685
|
+
```json
|
|
686
|
+
{
|
|
687
|
+
"q": {
|
|
688
|
+
"env": {
|
|
689
|
+
"XDG_DATA_HOME": "/home/you/.local/share",
|
|
690
|
+
"KIRO_HOME": "/home/you/.kiro"
|
|
691
|
+
},
|
|
692
|
+
"default": true
|
|
693
|
+
},
|
|
694
|
+
"k+": {
|
|
695
|
+
"env": {
|
|
696
|
+
"XDG_DATA_HOME": "/home/you/.local/share/brainiac/k+",
|
|
697
|
+
"KIRO_HOME": "/home/you/.kiro-accounts/k+"
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
Exactly one profile should be marked `"default": true` — it applies when no `[profile:X]` tag is present.
|
|
704
|
+
|
|
705
|
+
### Model selection
|
|
706
|
+
|
|
707
|
+
Model selection is a **cli-provider concern**, not a profile concern. kiro-cli accepts `--model <id>` per run, so the kiro-cli provider uses `"model_flag": "--model"`. A model tag (`[opus]`, `[sol]`, etc.) only affects that dispatch (and the thread/work item it was sent in). It never changes the account's saved `chat.defaultModel`, so other threads are unaffected.
|
|
708
|
+
|
|
709
|
+
Each account's `chat.defaultModel` (in its own `KIRO_HOME`) only applies when no model is resolved at all. Keep it set to a model that account can actually use.
|
|
710
|
+
|
|
711
|
+
**`settings_model_cmd` (legacy, avoid for kiro-cli):** for CLIs with no runtime model flag, a provider can set `"model_flag": ""` plus `"settings_model_cmd": [...]`, and brainiac runs that command before each tagged dispatch. That write is *persistent*: it becomes the account's default for every later dispatch, including unrelated threads. If a tag writes a model the default account doesn't have (e.g. `[sol]` landing on `q`), every untagged dispatch on that account fails with `The model '...' is not available`.
|
|
712
|
+
|
|
713
|
+
### Seeding a new KIRO_HOME
|
|
714
|
+
|
|
715
|
+
A fresh `KIRO_HOME` starts empty. brainiac dispatches with `--agent <name>`, so each home needs agent definitions or dispatches will fail. Symlink the shared bits from your real `~/.kiro` and keep only `settings/cli.json` per-account:
|
|
716
|
+
|
|
717
|
+
```bash
|
|
718
|
+
mkdir -p ~/.kiro-accounts/k+/settings
|
|
719
|
+
ln -s ~/.kiro/agents ~/.kiro-accounts/k+/agents
|
|
720
|
+
ln -s ~/.kiro/mcp.json ~/.kiro-accounts/k+/mcp.json
|
|
721
|
+
ln -s ~/.kiro/settings/permissions.yaml ~/.kiro-accounts/k+/settings/permissions.yaml
|
|
722
|
+
KIRO_HOME=~/.kiro-accounts/k+ kiro-cli settings chat.defaultModel claude-opus-5
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
Everything stays in sync with `~/.kiro` via the symlinks — only the model differs.
|
|
726
|
+
|
|
664
727
|
## Cron (Scheduled Tasks)
|
|
665
728
|
|
|
666
729
|
Agents can be dispatched on a schedule — daily standups, weekly summaries, periodic code reviews, whatever you want. Jobs are stored in `~/.brainiac/cron.json` and run in a background thread inside `brainiac server`.
|
data/lib/brainiac/helpers.rb
CHANGED
|
@@ -35,7 +35,7 @@ def load_cli_provider(provider_name)
|
|
|
35
35
|
%w[prompt_flag list_models_command resume_flag resume_args resume_id_flag new_session_id_flag
|
|
36
36
|
session_list_command session_id_field session_directory_field session_updated_field
|
|
37
37
|
session_list_path session_dir output_last_message_flag cwd_flag config_override_flag
|
|
38
|
-
effort_config_key effort_map title_flag heaviness_probe].each do |key|
|
|
38
|
+
effort_config_key effort_map title_flag heaviness_probe settings_model_cmd].each do |key|
|
|
39
39
|
next unless raw[key]
|
|
40
40
|
next if raw[key].respond_to?(:empty?) && raw[key].empty?
|
|
41
41
|
|
|
@@ -315,6 +315,11 @@ end
|
|
|
315
315
|
def register_work_item(branch:, worktree: nil, project: nil, agent: nil, source: nil, source_data: {})
|
|
316
316
|
map = load_work_item_map
|
|
317
317
|
|
|
318
|
+
# Normalize source to a string key — callers may pass a symbol (e.g. :fizzy).
|
|
319
|
+
# The sources hash is JSON-serialized with string keys, so mixing symbol and
|
|
320
|
+
# string keys produces a "duplicate key" error in JSON.pretty_generate.
|
|
321
|
+
source = source.to_s if source
|
|
322
|
+
|
|
318
323
|
# Check if a work item already exists for this branch
|
|
319
324
|
existing_id = nil
|
|
320
325
|
map.each do |wid, info|
|
|
@@ -330,7 +335,7 @@ def register_work_item(branch:, worktree: nil, project: nil, agent: nil, source:
|
|
|
330
335
|
# Update existing entry — merge in new source, update worktree/agent if provided
|
|
331
336
|
map[work_item_id]["worktree"] = worktree if worktree
|
|
332
337
|
map[work_item_id]["agent"] = agent if agent
|
|
333
|
-
map[work_item_id]["sources"]
|
|
338
|
+
map[work_item_id]["sources"] = normalize_source_keys(map[work_item_id]["sources"])
|
|
334
339
|
map[work_item_id]["sources"][source] = source_data if source
|
|
335
340
|
else
|
|
336
341
|
# Create new entry
|
|
@@ -440,6 +445,10 @@ end
|
|
|
440
445
|
def register_work_item_source(source:, source_data:, work_item_id: nil, branch: nil) # rubocop:disable Naming/PredicateMethod
|
|
441
446
|
map = load_work_item_map
|
|
442
447
|
|
|
448
|
+
# Normalize source to a string key — callers may pass a symbol (e.g. :fizzy).
|
|
449
|
+
# See register_work_item for why mixing symbol/string keys breaks JSON serialization.
|
|
450
|
+
source = source.to_s if source
|
|
451
|
+
|
|
443
452
|
# Find by ID or branch
|
|
444
453
|
target_id = work_item_id
|
|
445
454
|
unless target_id
|
|
@@ -453,12 +462,24 @@ def register_work_item_source(source:, source_data:, work_item_id: nil, branch:
|
|
|
453
462
|
|
|
454
463
|
return false unless target_id && map[target_id]
|
|
455
464
|
|
|
456
|
-
map[target_id]["sources"]
|
|
465
|
+
map[target_id]["sources"] = normalize_source_keys(map[target_id]["sources"])
|
|
457
466
|
map[target_id]["sources"][source] = source_data
|
|
458
467
|
save_work_item_map(map)
|
|
459
468
|
true
|
|
460
469
|
end
|
|
461
470
|
|
|
471
|
+
# Normalize a work item's sources hash so all keys are strings. Guards against
|
|
472
|
+
# a previously-corrupted in-memory hash that mixed symbol and string keys
|
|
473
|
+
# (e.g. both :fizzy and "fizzy"), which would otherwise crash JSON serialization.
|
|
474
|
+
# A later entry (symbol source just written) wins over an existing string entry.
|
|
475
|
+
def normalize_source_keys(sources)
|
|
476
|
+
return {} unless sources.is_a?(Hash)
|
|
477
|
+
|
|
478
|
+
sources.each_with_object({}) do |(key, value), acc|
|
|
479
|
+
acc[key.to_s] = value
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
462
483
|
def slugify(title, max_length: 40)
|
|
463
484
|
title.downcase.gsub(/[^a-z0-9\s-]/, "").strip.gsub(/\s+/, "-").slice(0, max_length).chomp("-")
|
|
464
485
|
end
|
|
@@ -774,12 +795,13 @@ end
|
|
|
774
795
|
# Plugins should NOT build their own resume logic; pass `resume: true` and let core handle it.
|
|
775
796
|
def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil, effort: nil, agent_name: nil, card_number: nil, comment_id: nil,
|
|
776
797
|
source: nil, source_context: {}, skip_column_move: false, cli_provider: nil, resume: false,
|
|
777
|
-
message: nil, channel: nil, context: nil, env: {}, profile: nil)
|
|
798
|
+
message: nil, channel: nil, context: nil, env: {}, profile: nil, explicit_model: nil)
|
|
778
799
|
# Intent gate: if a raw message is provided, check whether the agent should respond.
|
|
779
800
|
return nil if intent_skip?(message, agent_name: agent_name, source: source, channel: channel, context: context)
|
|
780
801
|
|
|
781
802
|
resolved = resolve_project_cli_config(project_config, cli_provider_override: cli_provider, agent_name: agent_name)
|
|
782
803
|
chdir ||= resolved["repo_path"]
|
|
804
|
+
explicit_model = model_explicit?(model, explicit_model)
|
|
783
805
|
model ||= resolved["agent_model"]
|
|
784
806
|
effort ||= resolved["agent_effort"]
|
|
785
807
|
agent_config_name = agent_name&.downcase&.gsub(/[^a-z0-9-]/, "-")
|
|
@@ -810,6 +832,12 @@ def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil
|
|
|
810
832
|
# wins over agent env; the default profile is only a fallback. See profiles.rb.
|
|
811
833
|
spawn_env = profile_spawn_env(agent_env_for(agent_name), profile).merge(env)
|
|
812
834
|
|
|
835
|
+
# For providers that use settings_model_cmd instead of a --model flag (e.g. kiro-cli,
|
|
836
|
+
# which requires `kiro-cli settings chat.defaultModel <name>`), apply the model to
|
|
837
|
+
# the provider's settings store before spawning. The write runs under spawn_env so it
|
|
838
|
+
# targets the correct account's KIRO_HOME. No-op when model_flag is non-empty.
|
|
839
|
+
apply_settings_model(model, resolved, spawn_env, explicit: explicit_model)
|
|
840
|
+
|
|
813
841
|
log_agent_launch(resolved: resolved, chdir: chdir, log_file: log_file, prompt_file: prompt_file,
|
|
814
842
|
output_file: output_file, cmd: cmd, should_resume: should_resume,
|
|
815
843
|
spawn_env: spawn_env, agent_name: agent_name)
|
|
@@ -829,7 +857,8 @@ def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil
|
|
|
829
857
|
card_number: card_number, skip_column_move: skip_column_move,
|
|
830
858
|
output_file: output_file, resolved: resolved, minted_session_id: minted_session_id,
|
|
831
859
|
head_before: head_before, status_before: status_before,
|
|
832
|
-
project_key_for_restart: project_key_for_restart, model: model, started_at: started_at
|
|
860
|
+
project_key_for_restart: project_key_for_restart, model: model, started_at: started_at,
|
|
861
|
+
profile: effective_profile_name(profile)
|
|
833
862
|
)
|
|
834
863
|
|
|
835
864
|
LOG.info "#{resolved["agent_cli"]} started (pid: #{pid}, agent: #{agent_config_name || "default"}, " \
|
|
@@ -946,6 +975,61 @@ def append_model_to_cmd(cmd, model, resolved)
|
|
|
946
975
|
cmd.push(resolved["agent_model_flag"], effective_model) if is_known
|
|
947
976
|
end
|
|
948
977
|
|
|
978
|
+
# A model is only "explicit" if the caller actually supplied one (i.e. it came from a
|
|
979
|
+
# tag) AND flagged it explicit. A nil model means we'll fall back to the project default,
|
|
980
|
+
# which must never be treated as explicit — otherwise no-tag dispatches would write it.
|
|
981
|
+
def model_explicit?(model, explicit_flag)
|
|
982
|
+
explicit_flag && !model.nil?
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
# When a provider uses settings_model_cmd instead of a runtime --model flag,
|
|
986
|
+
# run that command synchronously before spawning the agent so the correct model
|
|
987
|
+
# is already persisted in the provider's settings dir (e.g. KIRO_HOME/settings/cli.json).
|
|
988
|
+
#
|
|
989
|
+
# WARNING: this write is sticky. It changes the account's default for every later
|
|
990
|
+
# dispatch, not just this one, so a tag in one thread leaks into unrelated threads.
|
|
991
|
+
# Only use it for CLIs with no per-run model flag. kiro-cli supports --model and
|
|
992
|
+
# should use "model_flag": "--model" instead.
|
|
993
|
+
#
|
|
994
|
+
# Only fires when:
|
|
995
|
+
# - model_flag is absent or empty (the provider doesn't support runtime model selection)
|
|
996
|
+
# - settings_model_cmd is configured on the provider
|
|
997
|
+
# - a model was resolved (nil model = let the account default stand)
|
|
998
|
+
#
|
|
999
|
+
# The `explicit` flag decides whether "auto" gets written:
|
|
1000
|
+
# - explicit == true → the model came from an inline/card tag ([opus], [auto], ...).
|
|
1001
|
+
# ALWAYS write it, including "auto" — an explicit [auto] is a deliberate reset that
|
|
1002
|
+
# clears a polluted default back to the account's no-preference state.
|
|
1003
|
+
# - explicit == false → the model is the project's default fallback (no tag given).
|
|
1004
|
+
# Skip "auto" (writing it would clobber each account's per-account default on every
|
|
1005
|
+
# no-tag dispatch). A non-auto default still writes, preserving prior behavior.
|
|
1006
|
+
# - explicit == nil → caller didn't specify (older/other call sites). Treat as
|
|
1007
|
+
# not-explicit for safety — same as the pre-existing "skip auto" behavior.
|
|
1008
|
+
#
|
|
1009
|
+
# spawn_env is passed in so the command runs under the same env as the agent
|
|
1010
|
+
# (picks up KIRO_HOME, XDG_DATA_HOME, etc.) — ensuring the write goes to the right
|
|
1011
|
+
# account's settings dir. With per-profile KIRO_HOME the write is isolated to that
|
|
1012
|
+
# account, so concurrent dispatches on different profiles never race.
|
|
1013
|
+
def apply_settings_model(model, resolved, spawn_env, explicit: nil)
|
|
1014
|
+
return unless model
|
|
1015
|
+
return if resolved["agent_model_flag"] && !resolved["agent_model_flag"].empty?
|
|
1016
|
+
|
|
1017
|
+
settings_cmd = resolved["settings_model_cmd"]
|
|
1018
|
+
return unless settings_cmd.is_a?(Array) && settings_cmd.length >= 2
|
|
1019
|
+
|
|
1020
|
+
allowed = resolved["allowed_models"] || {}
|
|
1021
|
+
effective_model = allowed.key?(model) ? allowed[model] : model
|
|
1022
|
+
is_known = allowed.value?(effective_model) || allowed.key?(effective_model)
|
|
1023
|
+
return unless is_known
|
|
1024
|
+
# "auto" only writes when it was an explicit tag (used as a reset). A defaulted
|
|
1025
|
+
# "auto" is skipped so no-tag dispatches never overwrite an account's default.
|
|
1026
|
+
return if effective_model == "auto" && !explicit
|
|
1027
|
+
|
|
1028
|
+
full_cmd = settings_cmd + [effective_model]
|
|
1029
|
+
LOG.info "Pre-dispatch: setting model via #{full_cmd.join(" ")} (env: #{spawn_env.keys.join(", ")})"
|
|
1030
|
+
system(spawn_env, *full_cmd)
|
|
1031
|
+
end
|
|
1032
|
+
|
|
949
1033
|
# Resume via --session <id> (OpenCode), a bare flag (grok -c), or resume_args (already applied).
|
|
950
1034
|
def append_resume_to_cmd(cmd, resume)
|
|
951
1035
|
case resume
|
|
@@ -1232,23 +1316,39 @@ def check_brainiac_restart(head_before, status_before, chdir, project_key_for_re
|
|
|
1232
1316
|
end
|
|
1233
1317
|
|
|
1234
1318
|
def detect_model(project_config, tags: [], text: "", cli_provider_override: nil, agent_name: nil)
|
|
1319
|
+
detect_model_explicit(project_config, tags: tags, text: text,
|
|
1320
|
+
cli_provider_override: cli_provider_override, agent_name: agent_name).first
|
|
1321
|
+
end
|
|
1322
|
+
|
|
1323
|
+
# Like detect_model, but also reports whether the model came from an EXPLICIT source
|
|
1324
|
+
# (an inline [model] tag or a card tag) versus the project's default fallback.
|
|
1325
|
+
#
|
|
1326
|
+
# Returns [model_string, explicit_bool].
|
|
1327
|
+
# explicit == true → the user asked for this model via a tag ([opus], [auto], etc.)
|
|
1328
|
+
# explicit == false → no tag matched; the value is the project's agent_model default
|
|
1329
|
+
#
|
|
1330
|
+
# This distinction is load-bearing for apply_settings_model: an explicit tag should
|
|
1331
|
+
# always write the provider's default-model setting (even "auto", used as a reset),
|
|
1332
|
+
# while a fell-back-to-default value must NOT write — otherwise every no-tag dispatch
|
|
1333
|
+
# would clobber each account's per-account default back to the project default.
|
|
1334
|
+
def detect_model_explicit(project_config, tags: [], text: "", cli_provider_override: nil, agent_name: nil)
|
|
1235
1335
|
# If no explicit CLI provider override, check if the agent has one configured
|
|
1236
1336
|
effective_cli_provider = cli_provider_override || agent_cli_provider_for(agent_name)
|
|
1237
1337
|
resolved = resolve_project_cli_config(project_config, cli_provider_override: effective_cli_provider, agent_name: agent_name)
|
|
1238
1338
|
allowed_models = resolved["allowed_models"] || {}
|
|
1239
|
-
return resolved["agent_model"] if allowed_models.empty?
|
|
1339
|
+
return [resolved["agent_model"], false] if allowed_models.empty?
|
|
1240
1340
|
|
|
1241
1341
|
if (match = text.match(/\[(\w+)\]/))
|
|
1242
1342
|
key = match[1].downcase
|
|
1243
|
-
return allowed_models[key] if allowed_models.key?(key)
|
|
1343
|
+
return [allowed_models[key], true] if allowed_models.key?(key)
|
|
1244
1344
|
end
|
|
1245
1345
|
|
|
1246
1346
|
tags.each do |tag|
|
|
1247
1347
|
key = (tag.is_a?(Hash) ? tag["name"] : tag).to_s.downcase
|
|
1248
|
-
return allowed_models[key] if allowed_models.key?(key)
|
|
1348
|
+
return [allowed_models[key], true] if allowed_models.key?(key)
|
|
1249
1349
|
end
|
|
1250
1350
|
|
|
1251
|
-
resolved["agent_model"]
|
|
1351
|
+
[resolved["agent_model"], false]
|
|
1252
1352
|
end
|
|
1253
1353
|
|
|
1254
1354
|
# Detect effort level from inline tags [effort:high] or card tags (effort-high).
|
|
@@ -1274,6 +1374,40 @@ def detect_effort(project_config, tags: [], text: "", cli_provider_override: nil
|
|
|
1274
1374
|
resolved["agent_effort"]
|
|
1275
1375
|
end
|
|
1276
1376
|
|
|
1377
|
+
# Detect a profile ([profile:X]/[p:X]) from inline text OR card tags.
|
|
1378
|
+
#
|
|
1379
|
+
# A profile is a named env bundle (e.g. an alternate kiro-cli account via
|
|
1380
|
+
# XDG_DATA_HOME/KIRO_HOME) — see profiles.rb. This mirrors detect_model/detect_effort:
|
|
1381
|
+
# it checks inline text first, then falls back to card tags. Without the tag path,
|
|
1382
|
+
# adding a `p:k+` chip to a Fizzy card was silently ignored (only inline title text
|
|
1383
|
+
# was honored), so dispatches ran on the default profile.
|
|
1384
|
+
#
|
|
1385
|
+
# Card tags can be named either "profile:X"/"p:X" (colon form, matches inline syntax)
|
|
1386
|
+
# or a bare "X" that happens to be a known profile name.
|
|
1387
|
+
# Returns the lowercased profile name or nil.
|
|
1388
|
+
def detect_profile(text: "", tags: [])
|
|
1389
|
+
# Inline tag: [profile:k+] / [p:k+] — works in any channel
|
|
1390
|
+
inline = parse_inline_tags(text.to_s)[:profile]
|
|
1391
|
+
return inline if inline
|
|
1392
|
+
|
|
1393
|
+
known = defined?(PROFILES) ? PROFILES : {}
|
|
1394
|
+
|
|
1395
|
+
tags.each do |tag|
|
|
1396
|
+
name = (tag.is_a?(Hash) ? tag["name"] : tag).to_s.strip.downcase
|
|
1397
|
+
next if name.empty?
|
|
1398
|
+
|
|
1399
|
+
# "profile:k+" or "p:k+" colon form
|
|
1400
|
+
if (m = name.match(/\A(?:profile|p):(.+)\z/))
|
|
1401
|
+
return m[1].strip
|
|
1402
|
+
end
|
|
1403
|
+
|
|
1404
|
+
# bare tag that matches a configured profile name (e.g. a "k+" chip)
|
|
1405
|
+
return name if known.key?(name)
|
|
1406
|
+
end
|
|
1407
|
+
|
|
1408
|
+
nil
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1277
1411
|
# If a level isn't in allowed_efforts, return the closest lower level.
|
|
1278
1412
|
def resolve_effort_level(level, allowed)
|
|
1279
1413
|
all_levels = %w[low medium high xhigh max]
|
data/lib/brainiac/profiles.rb
CHANGED
|
@@ -8,17 +8,40 @@
|
|
|
8
8
|
# dispatch runs against via an inline tag ([profile:X] / [p:X]) — same CLI, same
|
|
9
9
|
# agent, different credentials.
|
|
10
10
|
#
|
|
11
|
+
# kiro-cli splits its state across two independent env vars — a profile should set both:
|
|
12
|
+
#
|
|
13
|
+
# XDG_DATA_HOME → auth store (kiro-cli/data.sqlite3): tokens + which account you're
|
|
14
|
+
# logged into. This selects the account.
|
|
15
|
+
# KIRO_HOME → settings dir (~/.kiro, incl. settings/cli.json): chat.defaultModel,
|
|
16
|
+
# agents, MCP, permissions, steering. This is where chat.defaultModel
|
|
17
|
+
# lives — giving each profile its own KIRO_HOME means each account has
|
|
18
|
+
# its own model setting.
|
|
19
|
+
#
|
|
20
|
+
# Model selection is a cli-provider concern, not a profile concern. kiro-cli takes
|
|
21
|
+
# --model per run ("model_flag": "--model"), so an inline model tag only affects that
|
|
22
|
+
# dispatch. Each account's chat.defaultModel (in its own KIRO_HOME) is just the fallback
|
|
23
|
+
# when no model resolves — keep it set to a model that account actually has.
|
|
24
|
+
#
|
|
25
|
+
# NOTE: a fresh KIRO_HOME starts empty. brainiac dispatches with --agent <name>, so each
|
|
26
|
+
# KIRO_HOME needs agent definitions (and any MCP/permissions you rely on) or dispatches
|
|
27
|
+
# break. Seed a new home by symlinking the shared bits from ~/.kiro and keeping only
|
|
28
|
+
# settings/cli.json per-account:
|
|
29
|
+
#
|
|
30
|
+
# mkdir -p ~/.kiro-accounts/k+/settings
|
|
31
|
+
# ln -s ~/.kiro/agents ~/.kiro-accounts/k+/agents
|
|
32
|
+
# ln -s ~/.kiro/mcp.json ~/.kiro-accounts/k+/mcp.json
|
|
33
|
+
# ln -s ~/.kiro/settings/permissions.yaml ~/.kiro-accounts/k+/settings/permissions.yaml
|
|
34
|
+
# KIRO_HOME=~/.kiro-accounts/k+ kiro-cli settings chat.defaultModel claude-opus-5
|
|
35
|
+
#
|
|
11
36
|
# Config lives at ~/.brainiac/profiles.json:
|
|
12
37
|
#
|
|
13
38
|
# {
|
|
14
|
-
# "q": { "env": { "XDG_DATA_HOME": "/home/andy/.local/share"
|
|
15
|
-
#
|
|
39
|
+
# "q": { "env": { "XDG_DATA_HOME": "/home/andy/.local/share",
|
|
40
|
+
# "KIRO_HOME": "/home/andy/.kiro" }, "default": true },
|
|
41
|
+
# "k+": { "env": { "XDG_DATA_HOME": "/home/andy/.local/share/brainiac/k+",
|
|
42
|
+
# "KIRO_HOME": "/home/andy/.kiro-accounts/k+" } }
|
|
16
43
|
# }
|
|
17
44
|
#
|
|
18
|
-
# A profile may also optionally pin a model and/or cli_provider:
|
|
19
|
-
#
|
|
20
|
-
# "k+": { "env": {...}, "model": "opus", "cli_provider": "kiro-pro" }
|
|
21
|
-
#
|
|
22
45
|
# Exactly one profile may be marked "default": true — it applies when no [profile:X]
|
|
23
46
|
# tag is present. Because it's only a fallback, an agent's own env (from agents.json)
|
|
24
47
|
# takes precedence over the default profile. An *explicitly requested* profile, by
|
|
@@ -70,6 +93,23 @@ def default_profile
|
|
|
70
93
|
PROFILES.find { |_name, entry| entry.is_a?(Hash) && entry["default"] }
|
|
71
94
|
end
|
|
72
95
|
|
|
96
|
+
# Resolve the *effective* profile name that a dispatch actually ran under, mirroring
|
|
97
|
+
# profile_spawn_env's fallback logic. This is what should be recorded in durable
|
|
98
|
+
# session history and shown in the monitor so you can tell whether a session used the
|
|
99
|
+
# default account (e.g. "q") or an explicitly-requested one (e.g. "k+").
|
|
100
|
+
#
|
|
101
|
+
# - An explicitly-requested, known profile → that name (normalized lowercase).
|
|
102
|
+
# - An explicitly-requested but UNKNOWN profile → nil (it was ignored at spawn, so it
|
|
103
|
+
# didn't actually run under any profile — matches profile_spawn_env dropping it).
|
|
104
|
+
# - No profile requested → the default profile's name, or nil if there's no default.
|
|
105
|
+
def effective_profile_name(profile_name)
|
|
106
|
+
if profile_name
|
|
107
|
+
profile_entry(profile_name) ? profile_name.to_s.downcase : nil
|
|
108
|
+
else
|
|
109
|
+
default_profile&.first
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
73
113
|
# Resolve the env hash contributed by a profile.
|
|
74
114
|
#
|
|
75
115
|
# When profile_name is given and matches a profile, returns that profile's env.
|
|
@@ -89,6 +89,7 @@ def archive_session_history(ctx:, exit_status:, signaled:)
|
|
|
89
89
|
"log_file" => ctx[:log_file],
|
|
90
90
|
"cli" => ctx[:agent_cli],
|
|
91
91
|
"model" => ctx[:model],
|
|
92
|
+
"profile" => ctx[:profile],
|
|
92
93
|
"started_at" => started_at&.utc&.iso8601,
|
|
93
94
|
"finished_at" => finished_at.utc.iso8601,
|
|
94
95
|
"duration_seconds" => started_at ? (finished_at - started_at).to_i : nil,
|
data/lib/brainiac/version.rb
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"q": {
|
|
3
3
|
"env": {
|
|
4
|
-
"XDG_DATA_HOME": "/home/you/.local/share"
|
|
5
|
-
|
|
4
|
+
"XDG_DATA_HOME": "/home/you/.local/share",
|
|
5
|
+
"KIRO_HOME": "/home/you/.kiro"
|
|
6
|
+
},
|
|
7
|
+
"default": true
|
|
6
8
|
},
|
|
7
9
|
"k+": {
|
|
8
10
|
"env": {
|
|
9
|
-
"XDG_DATA_HOME": "/home/you/.brainiac/
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
"XDG_DATA_HOME": "/home/you/.local/share/brainiac/k+",
|
|
12
|
+
"KIRO_HOME": "/home/you/.kiro-accounts/k+"
|
|
13
|
+
}
|
|
12
14
|
}
|
|
13
15
|
}
|