brainiac 0.1.3 → 0.1.5

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: be4dc2e28622b168e577e1813ab7bda98ce074fc04d0f4b63fe69502e24ef6f2
4
- data.tar.gz: 06513ef234e4a5ce4c23bc809c02942193a73394953a749df3b63653ceb70bc2
3
+ metadata.gz: e0e0e919150a76bcf0b7f289b72cfdbc8000bc0cacf94bc9103685cf49331415
4
+ data.tar.gz: 58174d5dcf9d0c3641f9a96317f6d94a4dda153481d586659275e8e81623fd24
5
5
  SHA512:
6
- metadata.gz: d8bedd4bef849857ac72d040d925bb74d23f97c10e575fdb8326c85d1580c60fbaff5e77a65d8958685c00b3b79f3b3d22e5b7304b194a5746e2aff74c4e850e
7
- data.tar.gz: c47e8739c7a655bde2283876538fd08d5b3b9500fd6bb56f38c2a02f4214d89b15be2f127f0752a78d68a4ddc13af0e7fb97a03d8db1a8fa1d43a61ac76495f1
6
+ metadata.gz: 73331c38b19e2ef1bb2d352b0a9b6cf6259d7075022e6ea529ad1203fd8dc30369d83b7d88e603c1ab04a59ec2a96856b01a59fc669a4b43c1efbba847ea2399
7
+ data.tar.gz: 917ea682b6749cef4a17d76a2a40d10deabb175c30af77044dc5a3d8be7132ab7f52e8722f8881bc11689c3d597ad83039e107e336cf40d891a0a222396df405
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brainiac (0.1.3)
4
+ brainiac (0.1.5)
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.3)
87
+ brainiac (0.1.5)
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
@@ -704,14 +704,11 @@ Exactly one profile should be marked `"default": true` — it applies when no `[
704
704
 
705
705
  ### Model selection
706
706
 
707
- Model selection is a **cli-provider concern**, not a profile concern. kiro-cli's backend doesn't support the `--model` runtime flag — model selection happens via `settings chat.defaultModel`. The kiro-cli provider is configured to use this approach:
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
708
 
709
- - `"model_flag": ""` — no `--model` flag is ever passed
710
- - `"settings_model_cmd": ["kiro-cli", "settings", "chat.defaultModel"]` — when a model tag (`[opus]`, `[sonnet]`, etc.) or Fizzy card tag resolves to a model, brainiac runs this command (with the profile's env) to set `chat.defaultModel` before spawning the dispatch
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.
711
710
 
712
- With per-profile `KIRO_HOME`, the settings write goes to that profile's own `settings/cli.json` — so `[p:k+] [opus]` updates k+'s model, and `[p:q] [sonnet]` updates q's model, independently.
713
-
714
- **Concurrency note:** the settings write is per-`KIRO_HOME`. Concurrent dispatches on *different* profiles never conflict. Two concurrent dispatches on the *same* profile with different model tags have a narrow write-before-spawn race — acceptable for single-user setups, but worth knowing.
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`.
715
712
 
716
713
  ### Seeding a new KIRO_HOME
717
714
 
@@ -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
@@ -348,10 +353,10 @@ def register_work_item(branch:, worktree: nil, project: nil, agent: nil, source:
348
353
  work_item_id
349
354
  end
350
355
 
351
- # Update dispatch overrides on a work item (cli_provider, model, effort).
356
+ # Update dispatch overrides on a work item (cli_provider, model, effort, profile).
352
357
  # Only non-nil values are stored; nil values are removed from existing overrides.
353
358
  # Returns true if the work item was found and updated, false otherwise.
354
- def update_work_item_overrides(cli_provider: nil, model: nil, effort: nil, work_item_id: nil, branch: nil) # rubocop:disable Naming/PredicateMethod
359
+ def update_work_item_overrides(cli_provider: nil, model: nil, effort: nil, profile: nil, work_item_id: nil, branch: nil) # rubocop:disable Naming/PredicateMethod
355
360
  map = load_work_item_map
356
361
 
357
362
  target_id = work_item_id
@@ -370,6 +375,7 @@ def update_work_item_overrides(cli_provider: nil, model: nil, effort: nil, work_
370
375
  overrides["cli_provider"] = cli_provider if cli_provider
371
376
  overrides["model"] = model if model
372
377
  overrides["effort"] = effort if effort
378
+ overrides["profile"] = profile if profile
373
379
  overrides.compact!
374
380
 
375
381
  if overrides.empty?
@@ -412,16 +418,26 @@ end
412
418
  # inline_cli_provider: — [cli:X] from current message (nil if not specified)
413
419
  # inline_model: — resolved model from current message (nil if not specified)
414
420
  # inline_effort: — [effort:X] from current message (nil if not specified)
421
+ # inline_profile: — [profile:X]/[p:X] from current message (nil if not specified)
422
+ #
423
+ # A profile sticks to the work item like model/effort do: [p:k+] once and every later
424
+ # untagged reply keeps running on k+ until a different [p:X] (e.g. [p:q]) replaces it.
425
+ # Unknown profile names are never persisted, so a typo can't pin a work item to a
426
+ # profile that doesn't exist (profile_spawn_env ignores it for this dispatch too).
415
427
  #
416
- # Returns: { cli_provider: String|nil, model: String|nil, effort: String|nil }
417
- def resolve_work_item_overrides(work_item_id: nil, branch: nil, inline_cli_provider: nil, inline_model: nil, inline_effort: nil)
428
+ # Returns: { cli_provider:, model:, effort:, profile: } (each String or nil)
429
+ def resolve_work_item_overrides(work_item_id: nil, branch: nil, inline_cli_provider: nil, inline_model: nil, inline_effort: nil,
430
+ inline_profile: nil)
418
431
  stored = work_item_overrides_for(work_item_id: work_item_id, branch: branch)
432
+ inline_profile = inline_profile&.to_s&.downcase
433
+ persist_profile = inline_profile && profile_entry(inline_profile) ? inline_profile : nil
419
434
 
420
435
  # Inline tags override stored values
421
436
  resolved = {
422
437
  cli_provider: inline_cli_provider || stored["cli_provider"],
423
438
  model: inline_model || stored["model"],
424
- effort: inline_effort || stored["effort"]
439
+ effort: inline_effort || stored["effort"],
440
+ profile: inline_profile || stored["profile"]
425
441
  }
426
442
 
427
443
  # Persist any new inline overrides to the work item for future dispatches
@@ -429,6 +445,7 @@ def resolve_work_item_overrides(work_item_id: nil, branch: nil, inline_cli_provi
429
445
  new_overrides[:cli_provider] = inline_cli_provider if inline_cli_provider
430
446
  new_overrides[:model] = inline_model if inline_model
431
447
  new_overrides[:effort] = inline_effort if inline_effort
448
+ new_overrides[:profile] = persist_profile if persist_profile
432
449
 
433
450
  update_work_item_overrides(work_item_id: work_item_id, branch: branch, **new_overrides) if new_overrides.any? && (work_item_id || branch)
434
451
 
@@ -440,6 +457,10 @@ end
440
457
  def register_work_item_source(source:, source_data:, work_item_id: nil, branch: nil) # rubocop:disable Naming/PredicateMethod
441
458
  map = load_work_item_map
442
459
 
460
+ # Normalize source to a string key — callers may pass a symbol (e.g. :fizzy).
461
+ # See register_work_item for why mixing symbol/string keys breaks JSON serialization.
462
+ source = source.to_s if source
463
+
443
464
  # Find by ID or branch
444
465
  target_id = work_item_id
445
466
  unless target_id
@@ -453,12 +474,24 @@ def register_work_item_source(source:, source_data:, work_item_id: nil, branch:
453
474
 
454
475
  return false unless target_id && map[target_id]
455
476
 
456
- map[target_id]["sources"] ||= {}
477
+ map[target_id]["sources"] = normalize_source_keys(map[target_id]["sources"])
457
478
  map[target_id]["sources"][source] = source_data
458
479
  save_work_item_map(map)
459
480
  true
460
481
  end
461
482
 
483
+ # Normalize a work item's sources hash so all keys are strings. Guards against
484
+ # a previously-corrupted in-memory hash that mixed symbol and string keys
485
+ # (e.g. both :fizzy and "fizzy"), which would otherwise crash JSON serialization.
486
+ # A later entry (symbol source just written) wins over an existing string entry.
487
+ def normalize_source_keys(sources)
488
+ return {} unless sources.is_a?(Hash)
489
+
490
+ sources.each_with_object({}) do |(key, value), acc|
491
+ acc[key.to_s] = value
492
+ end
493
+ end
494
+
462
495
  def slugify(title, max_length: 40)
463
496
  title.downcase.gsub(/[^a-z0-9\s-]/, "").strip.gsub(/\s+/, "-").slice(0, max_length).chomp("-")
464
497
  end
@@ -965,6 +998,11 @@ end
965
998
  # run that command synchronously before spawning the agent so the correct model
966
999
  # is already persisted in the provider's settings dir (e.g. KIRO_HOME/settings/cli.json).
967
1000
  #
1001
+ # WARNING: this write is sticky. It changes the account's default for every later
1002
+ # dispatch, not just this one, so a tag in one thread leaks into unrelated threads.
1003
+ # Only use it for CLIs with no per-run model flag. kiro-cli supports --model and
1004
+ # should use "model_flag": "--model" instead.
1005
+ #
968
1006
  # Only fires when:
969
1007
  # - model_flag is absent or empty (the provider doesn't support runtime model selection)
970
1008
  # - settings_model_cmd is configured on the provider
@@ -17,10 +17,10 @@
17
17
  # lives — giving each profile its own KIRO_HOME means each account has
18
18
  # its own model setting.
19
19
  #
20
- # Model selection is a cli-provider concern, not a profile concern. To stop passing
21
- # --model (e.g. because the CLI backend doesn't support the flag), set "model_flag": ""
22
- # in the cli-provider config (~/.brainiac/cli-providers/kiro.json). Each account's
23
- # chat.defaultModel (in its own KIRO_HOME) then drives model selection instead.
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
24
  #
25
25
  # NOTE: a fresh KIRO_HOME starts empty. brainiac dispatches with --agent <name>, so each
26
26
  # KIRO_HOME needs agent definitions (and any MCP/permissions you rely on) or dispatches
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Brainiac
4
4
  # @return [String] the current gem version
5
- VERSION = "0.1.3"
5
+ VERSION = "0.1.5"
6
6
  end
@@ -2,8 +2,7 @@
2
2
  "binary": "kiro-cli",
3
3
  "default_args": "chat --trust-all-tools --no-interactive",
4
4
  "agent_flag": "--agent",
5
- "model_flag": "",
6
- "settings_model_cmd": ["kiro-cli", "settings", "chat.defaultModel"],
5
+ "model_flag": "--model",
7
6
  "effort_flag": "--effort",
8
7
  "prompt_mode": "stdin",
9
8
  "resume_flag": "--resume",
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.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis