brindle 0.3.4__tar.gz → 0.3.5__tar.gz

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.
Files changed (129) hide show
  1. {brindle-0.3.4 → brindle-0.3.5}/.gitignore +2 -0
  2. {brindle-0.3.4 → brindle-0.3.5}/PKG-INFO +21 -17
  3. {brindle-0.3.4 → brindle-0.3.5}/README.md +20 -16
  4. {brindle-0.3.4 → brindle-0.3.5}/pyproject.toml +1 -1
  5. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/agents.py +18 -0
  6. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/autopilot.py +20 -10
  7. brindle-0.3.5/src/brindle/builtin_agents/developer-fable.md +7 -0
  8. brindle-0.3.5/src/brindle/builtin_agents/developer-gemini.md +7 -0
  9. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/developer-heavy.md +2 -2
  10. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/config.py +29 -10
  11. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/cost_estimate.py +8 -0
  12. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/cull.py +11 -0
  13. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/db.py +17 -0
  14. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/events.py +76 -11
  15. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/mcp_server.py +8 -8
  16. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/policy.py +15 -1
  17. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/learning.py +1 -1
  18. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/team_events.py +1 -1
  19. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/team_policy.py +8 -4
  20. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/tasks.py +1 -1
  21. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/usage.py +62 -0
  22. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/watch_costs.py +86 -9
  23. {brindle-0.3.4 → brindle-0.3.5}/LICENSE +0 -0
  24. {brindle-0.3.4 → brindle-0.3.5}/SCHEDULE-A +0 -0
  25. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/__init__.py +0 -0
  26. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/__main__.py +0 -0
  27. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/account.py +0 -0
  28. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/airgap.py +0 -0
  29. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/antigravity.py +0 -0
  30. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/budget.py +0 -0
  31. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/__init__.py +0 -0
  32. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/backend-auditor.md +0 -0
  33. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/developer-codex.md +0 -0
  34. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/developer-light.md +0 -0
  35. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/developer-local.md +0 -0
  36. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/developer.md +0 -0
  37. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/qa.md +0 -0
  38. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
  39. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
  40. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/reviewer-local.md +0 -0
  41. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/reviewer.md +0 -0
  42. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/subagent.md +0 -0
  43. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_agents/supervisor.md +0 -0
  44. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_rules/__init__.py +0 -0
  45. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_rules/security/backend.md +0 -0
  46. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
  47. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/builtin_rules/tests/only.md +0 -0
  48. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/ci_adapters.py +0 -0
  49. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/ci_client.py +0 -0
  50. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/ci_federation.py +0 -0
  51. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/ci_hosts.py +0 -0
  52. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/cli.py +0 -0
  53. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/codemap.py +0 -0
  54. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/codex_hook.py +0 -0
  55. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/company_identity.py +0 -0
  56. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/company_login.py +0 -0
  57. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/conflicts.py +0 -0
  58. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/cost.py +0 -0
  59. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/demo.py +0 -0
  60. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/detect.py +0 -0
  61. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/doctor.py +0 -0
  62. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/escapes.py +0 -0
  63. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/gates.py +0 -0
  64. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/git.py +0 -0
  65. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/guardrails.py +0 -0
  66. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/history.py +0 -0
  67. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/identity_lock.py +0 -0
  68. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/inbox.py +0 -0
  69. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/keys_cmds.py +0 -0
  70. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/keystore.py +0 -0
  71. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/launch_health.py +0 -0
  72. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/learned_rules.py +0 -0
  73. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/learning.py +0 -0
  74. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/legacy.py +0 -0
  75. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/model_access.py +0 -0
  76. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/model_availability.py +0 -0
  77. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/native/__init__.py +0 -0
  78. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/native/client.py +0 -0
  79. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/native/loop.py +0 -0
  80. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/native/permissions.py +0 -0
  81. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/native/runner.py +0 -0
  82. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/native/serve.py +0 -0
  83. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/native/tools.py +0 -0
  84. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pane_auth.py +0 -0
  85. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/permissions.py +0 -0
  86. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pipeline.py +0 -0
  87. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/plugins.py +0 -0
  88. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pool.py +0 -0
  89. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pricing.py +0 -0
  90. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/__init__.py +0 -0
  91. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/_files.py +0 -0
  92. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/account.py +0 -0
  93. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/audit_chain.py +0 -0
  94. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/audit_export.py +0 -0
  95. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/auth.py +0 -0
  96. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/brief_learning.py +0 -0
  97. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/cost_centers.py +0 -0
  98. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/credentials.py +0 -0
  99. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/features.py +0 -0
  100. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/keys.py +0 -0
  101. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/license.py +0 -0
  102. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/loopback.py +0 -0
  103. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/managed_models.py +0 -0
  104. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/org_profiles.py +0 -0
  105. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/orgkey.py +0 -0
  106. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/rollout.py +0 -0
  107. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/settings_sync.py +0 -0
  108. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/pro/status.py +0 -0
  109. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/procs.py +0 -0
  110. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/profiles.py +0 -0
  111. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/providers.py +0 -0
  112. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/quota.py +0 -0
  113. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/repo_cmds.py +0 -0
  114. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/repos.py +0 -0
  115. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/rewind.py +0 -0
  116. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/rule_checks.py +0 -0
  117. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/savings.py +0 -0
  118. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/scratch.py +0 -0
  119. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/secrets.py +0 -0
  120. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/self_serve.py +0 -0
  121. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/services.py +0 -0
  122. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/sessions.py +0 -0
  123. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/sidebar_follow.py +0 -0
  124. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/signin_pause.py +0 -0
  125. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/status_cache.py +0 -0
  126. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/tmux.py +0 -0
  127. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/view.py +0 -0
  128. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/watch.py +0 -0
  129. {brindle-0.3.4 → brindle-0.3.5}/src/brindle/workspaces.py +0 -0
@@ -12,3 +12,5 @@ docs/
12
12
  .brindle/bench.json
13
13
  .DS_Store
14
14
  commit_msg.txt
15
+ model_routing_plan.*
16
+ open_plan.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brindle
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges
5
5
  Project-URL: Homepage, https://pawdelta.com/brindle/
6
6
  Project-URL: Documentation, https://pawdelta.com/brindle/docs/recommended-use
@@ -89,7 +89,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
89
89
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
90
90
 
91
91
  `brindle --version` prints the installed version; the tmux status bar of every brindle
92
- session shows it too (`brindle 0.3.4`). A session started before an upgrade keeps
92
+ session shows it too (`brindle 0.3.5`). A session started before an upgrade keeps
93
93
  running the old code, and shows the old number, until you restart it.
94
94
 
95
95
  ## Quick start
@@ -531,7 +531,7 @@ Autopilot, merge gates and cleanup:
531
531
  | `max_agents` | `4` | workers running at once per session (`0`: no cap) |
532
532
  | `check_timeout` | `900` | seconds each check may take |
533
533
  | `check_concurrency` | `2` | check runs at once on this machine, across branches; the rest wait their turn (`0`: no cap). A run far past its last duration is reported to the supervisor |
534
- | `fable_escalation` | on, unless the worker runs on a cloud account (Bedrock, Vertex or Foundry) | retry a failed heavy task once on `developer-heavy` (Fable) before handing it to you; `true` turns it on for a cloud account, `false` off |
534
+ | `fable_escalation` | on, unless the worker runs on a cloud account (Bedrock, Vertex or Foundry) | retry a failed heavy task once on `developer-heavy` before handing it to you; `true` turns it on for a cloud account, `false` off |
535
535
  | `usage_limit` | `90` | autopilot stops pushing on at this % of your Claude usage limit |
536
536
  | `limit_cooldown_minutes` | `300` for Antigravity | how long a provider that hit its limit counts as unavailable |
537
537
  | `graphify` | if the graph is there | point agents at the repo's [graphify](https://github.com/safishamsi/graphify) code map (`false` turns it off) |
@@ -584,27 +584,30 @@ When a workspace is created (including from the pool), brindle starts one contai
584
584
 
585
585
  ### Routing by weight
586
586
 
587
- The supervisor sizes a task and passes `weight` (`"light"`, `"medium"` or
588
- `"heavy"`) to `assign`/`handoff`; brindle picks an available profile for that tier.
587
+ The supervisor sizes a task and passes `weight` (`"light"`, `"medium"`,
588
+ `"heavy"` or `"advanced"`, with `"reasoning"` and `"complex"` accepted as aliases) to
589
+ `assign`/`handoff`; brindle picks an available profile for that tier.
589
590
  Light is small, well-specified, mechanical work (docs, renames, simple tests);
590
- medium is a normal feature or bugfix in one area; heavy is design-heavy,
591
- cross-cutting work, subtle bugs or hard reasoning. The `routing` config maps each
592
- tier to profile names: the first is the tier's baseline, the rest are candidates
593
- hosted learning may pick instead (defaults shown; set one tier and the others
594
- keep theirs):
591
+ medium is a normal feature or bugfix in one area; heavy is architecture design,
592
+ broad cross-cutting refactors, or subtle bugs; advanced is complex, long-running
593
+ system-design and implementation tasks. The `routing`
594
+ config maps each tier to profile names: the first is the tier's baseline, the
595
+ rest are candidates hosted learning may pick instead (defaults shown; set one
596
+ tier and the others keep theirs):
595
597
 
596
598
  ```json
597
599
  {
598
600
  "routing": {
599
- "light": ["developer-light", "developer", "developer-codex", "developer-local"],
600
- "medium": ["developer", "developer-codex", "developer-heavy"],
601
- "heavy": ["developer", "developer-heavy", "developer-codex"]
601
+ "light": ["developer-light", "developer", "developer-codex", "developer-local"],
602
+ "medium": ["developer", "developer-codex", "developer-heavy"],
603
+ "heavy": ["developer-heavy", "developer-gemini", "developer", "developer-codex"],
604
+ "advanced": ["developer-fable", "developer-heavy", "developer-gemini"]
602
605
  }
603
606
  }
604
607
  ```
605
608
 
606
- No tier prefers another provider for its own sake: all-Claude or all-Codex may
607
- be what works best for you, and that's for the learner to find out.
609
+ No tier prefers another provider for its own sake: all-Claude, all-Codex or
610
+ all-Gemini may be what works best for you, and that's for the learner to find out.
608
611
 
609
612
  Routing merges per tier in this order: built-in defaults, `~/.brindle/config.json`
610
613
  (every repo), the repo's `.brindle/config.json` (shared with the team), then
@@ -622,8 +625,9 @@ each tier. Unknown profile names are skipped; doctor and the routing reply
622
625
  report the missing name and the file that supplied it.
623
626
 
624
627
  `developer-light` runs on Claude Haiku at low effort; `developer` on your Claude
625
- Code default model; `developer-codex` on Codex; `developer-heavy` on Claude Fable
626
- at high effort.
628
+ Code default model; `developer-codex` on Codex; `developer-heavy` on Claude Opus
629
+ at high effort; `developer-gemini` on Google Antigravity (Gemini) with high effort
630
+ (Tier 3); `developer-fable` on Claude Fable at high effort (Tier 4 advanced).
627
631
  A profile is skipped when its CLI isn't installed (`claude`, `codex`, `agy`), the
628
632
  local model server isn't answering, or its provider is at your `usage_limit`. If
629
633
  hosted learning is on it chooses among the profiles left; otherwise the first
@@ -59,7 +59,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
59
59
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
60
60
 
61
61
  `brindle --version` prints the installed version; the tmux status bar of every brindle
62
- session shows it too (`brindle 0.3.4`). A session started before an upgrade keeps
62
+ session shows it too (`brindle 0.3.5`). A session started before an upgrade keeps
63
63
  running the old code, and shows the old number, until you restart it.
64
64
 
65
65
  ## Quick start
@@ -501,7 +501,7 @@ Autopilot, merge gates and cleanup:
501
501
  | `max_agents` | `4` | workers running at once per session (`0`: no cap) |
502
502
  | `check_timeout` | `900` | seconds each check may take |
503
503
  | `check_concurrency` | `2` | check runs at once on this machine, across branches; the rest wait their turn (`0`: no cap). A run far past its last duration is reported to the supervisor |
504
- | `fable_escalation` | on, unless the worker runs on a cloud account (Bedrock, Vertex or Foundry) | retry a failed heavy task once on `developer-heavy` (Fable) before handing it to you; `true` turns it on for a cloud account, `false` off |
504
+ | `fable_escalation` | on, unless the worker runs on a cloud account (Bedrock, Vertex or Foundry) | retry a failed heavy task once on `developer-heavy` before handing it to you; `true` turns it on for a cloud account, `false` off |
505
505
  | `usage_limit` | `90` | autopilot stops pushing on at this % of your Claude usage limit |
506
506
  | `limit_cooldown_minutes` | `300` for Antigravity | how long a provider that hit its limit counts as unavailable |
507
507
  | `graphify` | if the graph is there | point agents at the repo's [graphify](https://github.com/safishamsi/graphify) code map (`false` turns it off) |
@@ -554,27 +554,30 @@ When a workspace is created (including from the pool), brindle starts one contai
554
554
 
555
555
  ### Routing by weight
556
556
 
557
- The supervisor sizes a task and passes `weight` (`"light"`, `"medium"` or
558
- `"heavy"`) to `assign`/`handoff`; brindle picks an available profile for that tier.
557
+ The supervisor sizes a task and passes `weight` (`"light"`, `"medium"`,
558
+ `"heavy"` or `"advanced"`, with `"reasoning"` and `"complex"` accepted as aliases) to
559
+ `assign`/`handoff`; brindle picks an available profile for that tier.
559
560
  Light is small, well-specified, mechanical work (docs, renames, simple tests);
560
- medium is a normal feature or bugfix in one area; heavy is design-heavy,
561
- cross-cutting work, subtle bugs or hard reasoning. The `routing` config maps each
562
- tier to profile names: the first is the tier's baseline, the rest are candidates
563
- hosted learning may pick instead (defaults shown; set one tier and the others
564
- keep theirs):
561
+ medium is a normal feature or bugfix in one area; heavy is architecture design,
562
+ broad cross-cutting refactors, or subtle bugs; advanced is complex, long-running
563
+ system-design and implementation tasks. The `routing`
564
+ config maps each tier to profile names: the first is the tier's baseline, the
565
+ rest are candidates hosted learning may pick instead (defaults shown; set one
566
+ tier and the others keep theirs):
565
567
 
566
568
  ```json
567
569
  {
568
570
  "routing": {
569
- "light": ["developer-light", "developer", "developer-codex", "developer-local"],
570
- "medium": ["developer", "developer-codex", "developer-heavy"],
571
- "heavy": ["developer", "developer-heavy", "developer-codex"]
571
+ "light": ["developer-light", "developer", "developer-codex", "developer-local"],
572
+ "medium": ["developer", "developer-codex", "developer-heavy"],
573
+ "heavy": ["developer-heavy", "developer-gemini", "developer", "developer-codex"],
574
+ "advanced": ["developer-fable", "developer-heavy", "developer-gemini"]
572
575
  }
573
576
  }
574
577
  ```
575
578
 
576
- No tier prefers another provider for its own sake: all-Claude or all-Codex may
577
- be what works best for you, and that's for the learner to find out.
579
+ No tier prefers another provider for its own sake: all-Claude, all-Codex or
580
+ all-Gemini may be what works best for you, and that's for the learner to find out.
578
581
 
579
582
  Routing merges per tier in this order: built-in defaults, `~/.brindle/config.json`
580
583
  (every repo), the repo's `.brindle/config.json` (shared with the team), then
@@ -592,8 +595,9 @@ each tier. Unknown profile names are skipped; doctor and the routing reply
592
595
  report the missing name and the file that supplied it.
593
596
 
594
597
  `developer-light` runs on Claude Haiku at low effort; `developer` on your Claude
595
- Code default model; `developer-codex` on Codex; `developer-heavy` on Claude Fable
596
- at high effort.
598
+ Code default model; `developer-codex` on Codex; `developer-heavy` on Claude Opus
599
+ at high effort; `developer-gemini` on Google Antigravity (Gemini) with high effort
600
+ (Tier 3); `developer-fable` on Claude Fable at high effort (Tier 4 advanced).
597
601
  A profile is skipped when its CLI isn't installed (`claude`, `codex`, `agy`), the
598
602
  local model server isn't answering, or its provider is at your `usage_limit`. If
599
603
  hosted learning is on it chooses among the profiles left; otherwise the first
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brindle"
3
- version = "0.3.4"
3
+ version = "0.3.5"
4
4
  description = "Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -2052,6 +2052,24 @@ def _review_profile(db: DB, ws: Workspace, profile: str | None, cfg: RepoConfig)
2052
2052
  f"reviewer profile {profile!r} can't run: {why}. "
2053
2053
  "Fix that, or set review_profile (or pass profile) to a different reviewer"
2054
2054
  )
2055
+ try:
2056
+ from brindle import policy as pol
2057
+ d = pol.check_assign(cfg, ws.repo_root, profile, "code review", "review")
2058
+ if not d.allowed:
2059
+ for fallback in (cfg.reviewer, "reviewer", "reviewer-gemini", "reviewer-local"):
2060
+ if fallback != profile:
2061
+ try:
2062
+ load_profile(fallback, ws.repo_root)
2063
+ fb_d = pol.check_assign(cfg, ws.repo_root, fallback, "code review", "review")
2064
+ if fb_d.allowed:
2065
+ return fallback
2066
+ except Exception:
2067
+ continue
2068
+ raise AgentError(f"reviewer profile {profile!r} refused by policy: {d.reason}")
2069
+ except AgentError:
2070
+ raise
2071
+ except Exception:
2072
+ pass
2055
2073
  return profile
2056
2074
 
2057
2075
 
@@ -81,9 +81,9 @@ to be asked each step.
81
81
  task's work must merge before another starts; brindle queues it until then.
82
82
  - Unless you need a specific profile, pass `weight` on every `assign`:
83
83
  "light" (small, well-specified, mechanical: docs, renames, simple tests),
84
- "medium" (normal feature work, refactors, cross-cutting edits, or bugfixes) or
85
- "heavy" (reserved for exceptionally complex algorithmic work, subtle bugs that
86
- failed earlier fix rounds, or deep reasoning). brindle picks an available
84
+ "medium" (normal feature work or bugfixes in one area),
85
+ "heavy" (architecture design, broad cross-cutting refactors, or subtle bugs that failed earlier fix rounds), or
86
+ "advanced" (complex, long-running system-design and implementation tasks). brindle picks an available
87
87
  profile for that tier.
88
88
  - Keep task briefs short: the goal in a sentence or two, the files, and the
89
89
  test that proves it done. Workers read the tests and code themselves;
@@ -506,7 +506,7 @@ def _route_by_weight(db: DB, cfg: RepoConfig, repo_root: str, weight: str, task:
506
506
  skipped: list[str] = []
507
507
  remaining: list[str] = []
508
508
  over: dict[str, str] = {}
509
- weight_order = {"light": 0, "medium": 1, "heavy": 2}
509
+ weight_order = {"light": 0, "medium": 1, "heavy": 2, "advanced": 3, "reasoning": 3, "complex": 3}
510
510
  effective_ceiling = ceiling or (cfg.max_weight if (cfg.max_weight and cfg.max_weight in weight_order) else None)
511
511
  order = cfg.routing.get(weight, [])
512
512
  if effective_ceiling:
@@ -597,6 +597,7 @@ def effective_profile_model(p, repo_root: str | None = None) -> str:
597
597
  return m.strip()
598
598
 
599
599
 
600
+
600
601
  def model_cost_info(name: str, cfg: RepoConfig | None = None, repo_root: str | None = None) -> str:
601
602
  """Format model name, effort, and cost tier as a cost signal string."""
602
603
  try:
@@ -608,7 +609,7 @@ def model_cost_info(name: str, cfg: RepoConfig | None = None, repo_root: str | N
608
609
  if p.effort:
609
610
  model_info.append(f"{p.effort} effort")
610
611
  pw = profile_weight(name, cfg, repo_root)
611
- if pw == "heavy":
612
+ if pw in ("heavy", "advanced", "reasoning"):
612
613
  cost_tier = "most expensive tier"
613
614
  elif pw == "light":
614
615
  cost_tier = "lowest cost tier"
@@ -621,7 +622,9 @@ def model_cost_info(name: str, cfg: RepoConfig | None = None, repo_root: str | N
621
622
 
622
623
 
623
624
  def profile_weight(name: str, cfg: RepoConfig | None = None, repo_root: str | None = None) -> str:
624
- """The tier/weight of a profile ('light', 'medium', 'heavy')."""
625
+ """The tier/weight of a profile ('light', 'medium', 'heavy', 'advanced')."""
626
+ if "-fable" in name or "-advanced" in name or "-reasoning" in name:
627
+ return "advanced"
625
628
  try:
626
629
  p = load_profile(name, repo_root)
627
630
  m = effective_profile_model(p, repo_root).lower()
@@ -647,6 +650,9 @@ def profile_weight(name: str, cfg: RepoConfig | None = None, repo_root: str | No
647
650
  in_light = name in cfg.routing.get("light", [])
648
651
  in_med = name in cfg.routing.get("medium", [])
649
652
  in_heavy = name in cfg.routing.get("heavy", [])
653
+ in_advanced = name in cfg.routing.get("advanced", []) or name in cfg.routing.get("reasoning", [])
654
+ if in_advanced and not in_heavy and not in_med and not in_light:
655
+ return "advanced"
650
656
  if in_heavy and not in_med and not in_light:
651
657
  return "heavy"
652
658
  if in_light and not in_med and not in_heavy:
@@ -670,7 +676,7 @@ def choose_profile(db: DB, caller_id: str, repo_root: str, requested: str | None
670
676
  filled with what ``brindle.savings`` records: the ``profile``, the
671
677
  ``baseline`` (the pick without learning), ``learned`` and the ``weight``."""
672
678
  cfg = load_repo_config(repo_root)
673
- weight_order = {"light": 0, "medium": 1, "heavy": 2}
679
+ weight_order = {"light": 0, "medium": 1, "heavy": 2, "advanced": 3, "reasoning": 3, "complex": 3}
674
680
  ceiling = cfg.max_weight if (cfg.max_weight and cfg.max_weight in weight_order) else None
675
681
 
676
682
  if requested:
@@ -700,20 +706,24 @@ def choose_profile(db: DB, caller_id: str, repo_root: str, requested: str | None
700
706
  why.append(f"milestone profile -> {name}{model_cost_info(name, cfg, repo_root)}")
701
707
  if not name:
702
708
  from brindle import learning
709
+ from brindle.config import normalize_weight
703
710
 
704
711
  if weight:
705
- if weight not in WEIGHTS:
712
+ norm_weight = normalize_weight(weight)
713
+ if norm_weight not in WEIGHTS:
706
714
  raise AutopilotError(f"weight must be one of {', '.join(WEIGHTS)}, not {weight!r}")
715
+ weight = norm_weight
707
716
  if ceiling and weight_order[weight] > weight_order[ceiling]:
708
717
  orig_weight = weight
709
718
  weight = ceiling
710
719
  if why is not None:
711
720
  why.append(f"weight {orig_weight} capped to {weight} by max_weight")
712
721
  if files and all(f.startswith("docs/") or f.endswith(".md") for f in files):
713
- if weight == "heavy":
722
+ if weight in ("advanced", "reasoning", "heavy"):
723
+ orig_demote = weight
714
724
  weight = "medium"
715
725
  if why is not None:
716
- why.append("weight demoted from heavy to medium (docs only)")
726
+ why.append(f"weight demoted from {orig_demote} to medium (docs only)")
717
727
  elif weight == "medium":
718
728
  weight = "light"
719
729
  if why is not None:
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: developer-fable
3
+ description: Implements a complex, long-running system-design or implementation task on its own branch, on Fable with high effort (the advanced tier of weight routing)
4
+ extends: developer
5
+ model: fable
6
+ effort: high
7
+ ---
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: developer-gemini
3
+ description: Implements a heavy coding or architectural task on its own branch, using Google Antigravity (Gemini) with high effort (the heavy tier of weight routing)
4
+ extends: developer
5
+ provider: antigravity
6
+ effort: high
7
+ ---
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: developer-heavy
3
- description: Implements a design-heavy or subtle task on its own branch, on Fable with high effort (the heavy tier of weight routing)
3
+ description: Implements a design-heavy or subtle task on its own branch, on Opus with high effort (the heavy tier of weight routing)
4
4
  extends: developer
5
- model: claude-fable-5-1
5
+ model: opus
6
6
  effort: high
7
7
  ---
@@ -43,15 +43,32 @@ def user_config_path() -> Path:
43
43
  return brindle_home() / CONFIG_FILE
44
44
 
45
45
 
46
- WEIGHTS = ("light", "medium", "heavy")
46
+ WEIGHTS = ("light", "medium", "heavy", "advanced")
47
+ WEIGHT_ALIASES = {
48
+ "reasoning": "advanced",
49
+ "complex": "advanced",
50
+ "complex reasoning": "advanced",
51
+ "deep": "advanced",
52
+ }
53
+
54
+
55
+ def normalize_weight(weight: str | None) -> str | None:
56
+ """Normalize a weight string, resolving aliases like 'reasoning' or 'complex' to 'advanced'."""
57
+ if not weight:
58
+ return None
59
+ w = weight.strip().lower()
60
+ return WEIGHT_ALIASES.get(w, w)
61
+
62
+
47
63
  # The first profile of each tier is its baseline; the rest are candidates
48
64
  # brindle Pro's learner may pick instead when its evidence says they do better.
49
- # Nothing here prefers another provider for its own sake: all-Claude or
50
- # all-Codex may well be best, and that's the learner's call.
65
+ # Nothing here prefers another provider for its own sake: all-Claude, all-Codex
66
+ # or all-Gemini may well be best, and that's the learner's call.
51
67
  DEFAULT_ROUTING = {
52
68
  "light": ["developer-light", "developer", "developer-codex", "developer-local"],
53
69
  "medium": ["developer", "developer-codex", "developer-heavy"],
54
- "heavy": ["developer", "developer-heavy", "developer-codex"],
70
+ "heavy": ["developer-heavy", "developer-gemini", "developer", "developer-codex"],
71
+ "advanced": ["developer-fable", "developer-heavy", "developer-gemini"],
55
72
  }
56
73
 
57
74
 
@@ -91,7 +108,7 @@ class RepoConfig:
91
108
  merge_into: str | None = None # branch worker branches are cut from and merge into (None: the supervisor's / default branch)
92
109
  auto_merge_default_branch: bool = False # let the pipeline merge into the repo's default branch on its own
93
110
  delegation: str = "balanced" # how readily a supervisor hands work to workers: "conservative" (save tokens), "balanced" or "fast" (save time)
94
- max_weight: str | None = None # ceiling for weight routing: "light", "medium", or None (unlimited)
111
+ max_weight: str | None = None # ceiling for weight routing: "light", "medium", "heavy", "advanced", or None (unlimited)
95
112
  plan_first: bool = False # workers propose a plan and wait for approval before editing
96
113
  # "on": brindle answers workers' permission requests by its rules (see
97
114
  # brindle.permissions). Unset, it's "on" for Pro and up and "off" on Free.
@@ -245,15 +262,17 @@ def load_repo_config(repo_root: str | Path) -> RepoConfig:
245
262
  routing = source.get("routing")
246
263
  if isinstance(routing, dict):
247
264
  for tier, names in routing.items():
248
- if tier in WEIGHTS and isinstance(names, list):
249
- cfg.routing[tier] = [n for n in names if isinstance(n, str)]
250
- cfg.routing_sources[tier] = str(path)
265
+ norm_tier = normalize_weight(tier)
266
+ if norm_tier in WEIGHTS and isinstance(names, list):
267
+ cfg.routing[norm_tier] = [n for n in names if isinstance(n, str)]
268
+ cfg.routing_sources[norm_tier] = str(path)
251
269
  for source in (shared, local): # per weight, so a repo can override one and keep the rest
252
270
  second = source.get("second_review")
253
271
  if isinstance(second, dict):
254
272
  for tier, name in second.items():
255
- if tier in WEIGHTS and isinstance(name, str) and name:
256
- cfg.second_review[tier] = name
273
+ norm_tier = normalize_weight(tier)
274
+ if norm_tier in WEIGHTS and isinstance(name, str) and name:
275
+ cfg.second_review[norm_tier] = name
257
276
  for source in (user, shared, local): # per model, so a repo can override one and keep the rest
258
277
  pricing = source.get("pricing")
259
278
  if isinstance(pricing, dict):
@@ -62,12 +62,20 @@ PRIOR: dict[tuple[str, str], tuple[Tokens, Tokens]] = {
62
62
  Tokens(120_000, 80_000, 800_000, 10_000_000)),
63
63
  ("worker", "heavy"): (Tokens(120_000, 90_000, 900_000, 12_000_000),
64
64
  Tokens(250_000, 180_000, 1_800_000, 25_000_000)),
65
+ ("worker", "advanced"): (Tokens(200_000, 150_000, 1_500_000, 20_000_000),
66
+ Tokens(450_000, 300_000, 3_000_000, 40_000_000)),
67
+ ("worker", "reasoning"): (Tokens(200_000, 150_000, 1_500_000, 20_000_000),
68
+ Tokens(450_000, 300_000, 3_000_000, 40_000_000)),
65
69
  ("review", "light"): (Tokens(15_000, 6_000, 80_000, 600_000),
66
70
  Tokens(30_000, 12_000, 160_000, 1_200_000)),
67
71
  ("review", "medium"): (Tokens(20_000, 10_000, 100_000, 1_000_000),
68
72
  Tokens(40_000, 20_000, 200_000, 2_000_000)),
69
73
  ("review", "heavy"): (Tokens(40_000, 20_000, 200_000, 2_500_000),
70
74
  Tokens(80_000, 40_000, 400_000, 5_000_000)),
75
+ ("review", "advanced"): (Tokens(60_000, 30_000, 300_000, 4_000_000),
76
+ Tokens(120_000, 60_000, 600_000, 8_000_000)),
77
+ ("review", "reasoning"): (Tokens(60_000, 30_000, 300_000, 4_000_000),
78
+ Tokens(120_000, 60_000, 600_000, 8_000_000)),
71
79
  }
72
80
  PRIOR_LOW, PRIOR_HIGH = 0.5, 1.5 # how far a prior's range is widened
73
81
 
@@ -143,6 +143,17 @@ def sweep(db: DB, now: float | None = None) -> list[str]:
143
143
 
144
144
  done.extend(signin_pause.sweep(db, now))
145
145
 
146
+ # 1g. Periodic cost events (Team org_budgets): report delta spend for supervisor and workers.
147
+ from brindle import events
148
+
149
+ roots = {ws.repo_root for ws in db.find_workspaces() if ws.repo_root}
150
+ for r in roots:
151
+ try:
152
+ cfg = load_repo_config(r)
153
+ done.extend(events.emit_periodic_cost(cfg, r))
154
+ except Exception:
155
+ pass
156
+
146
157
  # 2. Workers nobody needs any more.
147
158
  limits: dict[str, float] = {}
148
159
  for a in db.list_agents():
@@ -374,6 +374,11 @@ CREATE TABLE IF NOT EXISTS launch_health (
374
374
  baseline TEXT NOT NULL DEFAULT '',
375
375
  outcome TEXT
376
376
  );
377
+ CREATE TABLE IF NOT EXISTS agent_reported_spend (
378
+ agent_id TEXT PRIMARY KEY,
379
+ reported_usd REAL NOT NULL,
380
+ updated_at REAL NOT NULL
381
+ );
377
382
  """
378
383
 
379
384
 
@@ -1677,3 +1682,15 @@ class DB:
1677
1682
  cols = ", ".join(f"{k}=?" for k in fields)
1678
1683
  with self.tx() as c:
1679
1684
  c.execute(f"UPDATE tasks SET {cols} WHERE id=?", (*fields.values(), task_id))
1685
+
1686
+ def get_reported_spend(self, agent_id: str) -> float:
1687
+ row = self.conn.execute("SELECT reported_usd FROM agent_reported_spend WHERE agent_id=?", (agent_id,)).fetchone()
1688
+ return float(row[0]) if row else 0.0
1689
+
1690
+ def set_reported_spend(self, agent_id: str, reported_usd: float) -> None:
1691
+ with self.tx() as c:
1692
+ c.execute(
1693
+ "INSERT INTO agent_reported_spend (agent_id, reported_usd, updated_at) VALUES (?, ?, ?) "
1694
+ "ON CONFLICT(agent_id) DO UPDATE SET reported_usd=excluded.reported_usd, updated_at=excluded.updated_at",
1695
+ (agent_id, reported_usd, time.time())
1696
+ )
@@ -34,7 +34,7 @@ log = logging.getLogger(__name__)
34
34
 
35
35
  GROUP = plugins.EVENTS
36
36
  KINDS = ("assign", "handoff", "review", "escalated", "merge", "remove", "deny_assign",
37
- "deny_merge")
37
+ "deny_merge", "cost")
38
38
 
39
39
 
40
40
  @dataclass(frozen=True)
@@ -100,9 +100,9 @@ def _model(profile: str | None, repo_root: str) -> str | None:
100
100
 
101
101
 
102
102
  def _cost(kind: str, worker: Agent | None, repo_root: str) -> float | None:
103
- """What ``worker`` spent, for the ``remove`` event that ends it, when the
103
+ """What ``worker`` spent, for the ``remove`` or ``cost`` event that ends it or tracks spend, when the
104
104
  org counts spend (Team ``org_budgets``); None otherwise or when it can't be priced."""
105
- if kind != "remove" or worker is None:
105
+ if kind not in ("remove", "cost") or worker is None:
106
106
  return None
107
107
  try:
108
108
  from brindle import budget
@@ -111,8 +111,14 @@ def _cost(kind: str, worker: Agent | None, repo_root: str) -> float | None:
111
111
 
112
112
  if not license.has("org_budgets"):
113
113
  return None
114
- spent = budget.worker_spend(DB(), worker, repo_root)
115
- return round(spent, 4) if spent else None
114
+ db = DB()
115
+ spent = budget.worker_spend(db, worker, repo_root)
116
+ if spent is None:
117
+ return None
118
+ get_rep = getattr(db, "get_reported_spend", None)
119
+ reported = get_rep(worker.id) if callable(get_rep) else 0.0
120
+ delta = round(spent - (reported or 0.0), 4)
121
+ return delta if delta > 0 else None
116
122
  except Exception:
117
123
  log.debug("brindle: couldn't price %s for the event", worker.id, exc_info=True)
118
124
  return None
@@ -125,7 +131,7 @@ def _by_model(kind: str, worker: Agent | None, repo_root: str,
125
131
  (which can include live usage) exceeds what the history rows add up to,
126
132
  the remainder is an "unknown model" entry with no tokens, so the split
127
133
  sums to ``cost_usd``."""
128
- if kind != "remove" or worker is None:
134
+ if kind not in ("remove", "cost") or worker is None:
129
135
  return None
130
136
  try:
131
137
  from brindle import cost
@@ -146,10 +152,20 @@ def _by_model(kind: str, worker: Agent | None, repo_root: str,
146
152
  slot = out.setdefault(cost.model_label(p), {"tokens": 0, "usd": 0.0})
147
153
  slot["tokens"] += p.tokens
148
154
  slot["usd"] += p.dollars or 0.0
149
- rest = round((cost_usd or 0.0) - sum(b["usd"] for b in out.values()), 4)
150
- if rest > 0:
151
- slot = out.setdefault("unknown model", {"tokens": 0, "usd": 0.0})
152
- slot["usd"] += rest
155
+ total_usd = sum(b["usd"] for b in out.values())
156
+ if cost_usd is not None and total_usd > cost_usd > 0:
157
+ factor = cost_usd / total_usd
158
+ for b in out.values():
159
+ b["usd"] = round(b["usd"] * factor, 4)
160
+ diff = round(cost_usd - sum(b["usd"] for b in out.values()), 4)
161
+ if diff:
162
+ first = next(iter(out.values()))
163
+ first["usd"] = round(first["usd"] + diff, 4)
164
+ else:
165
+ rest = round((cost_usd or 0.0) - total_usd, 4)
166
+ if rest > 0:
167
+ slot = out.setdefault("unknown model", {"tokens": 0, "usd": 0.0})
168
+ slot["usd"] += rest
153
169
  return out or None
154
170
  except Exception:
155
171
  log.debug("brindle: couldn't split %s by model for the event", worker.id, exc_info=True)
@@ -173,10 +189,59 @@ def emit(cfg: RepoConfig, kind: str, ws: Workspace, worker: Agent | None = None,
173
189
  cost_usd=cost_usd,
174
190
  by_model=_by_model(kind, worker, ws.repo_root, cost_usd),
175
191
  ))
192
+ if cost_usd and worker and kind in ("remove", "cost"):
193
+ try:
194
+ from brindle.db import DB
195
+ db = DB()
196
+ set_rep = getattr(db, "set_reported_spend", None)
197
+ get_rep = getattr(db, "get_reported_spend", None)
198
+ if callable(set_rep) and callable(get_rep):
199
+ old_reported = get_rep(worker.id) or 0.0
200
+ set_rep(worker.id, round(old_reported + cost_usd, 4))
201
+ except Exception:
202
+ pass
176
203
  except Exception:
177
204
  log.exception("brindle: the events plugin failed on a %s event", kind)
178
205
 
179
206
 
207
+ def emit_periodic_cost(cfg: RepoConfig, repo_root: str) -> list[str]:
208
+ """Check active agents (workers and supervisor sessions) in ``repo_root``,
209
+ compute their delta spend since last report, and emit a 'cost' event
210
+ if delta >= 0.01."""
211
+ from brindle import budget
212
+ from brindle.db import DB
213
+ from brindle.pro import license
214
+
215
+ done: list[str] = []
216
+ try:
217
+ if not license.has("org_budgets"):
218
+ return done
219
+ except Exception:
220
+ return done
221
+
222
+ try:
223
+ db = DB()
224
+ for a in db.list_agents():
225
+ if a.dismissed_at is not None:
226
+ continue
227
+ ws = db.get_workspace(a.workspace_id)
228
+ if ws is None or ws.repo_root != repo_root:
229
+ continue
230
+ spent = budget.worker_spend(db, a, repo_root)
231
+ if spent is None:
232
+ continue
233
+ get_rep = getattr(db, "get_reported_spend", None)
234
+ reported = get_rep(a.id) if callable(get_rep) else 0.0
235
+ delta = round(spent - (reported or 0.0), 4)
236
+ if delta < 0.01:
237
+ continue
238
+ emit(cfg, "cost", ws, worker=a)
239
+ done.append(f"emitted periodic cost ${delta:.2f} for agent {a.id}")
240
+ except Exception:
241
+ log.exception("brindle: periodic cost check failed for %s", repo_root)
242
+ return done
243
+
244
+
180
245
  def emit_denial(cfg: RepoConfig, repo_root: str, what: str, reason: str, *,
181
246
  branch: str | None = None, profile: str | None = None,
182
247
  provider: str | None = None, model: str | None = None,
@@ -194,5 +259,5 @@ def emit_denial(cfg: RepoConfig, repo_root: str, what: str, reason: str, *,
194
259
  log.exception("brindle: the events plugin failed on a deny_%s event", what)
195
260
 
196
261
 
197
- __all__ = ["GROUP", "KINDS", "Event", "EventsPlugin", "emit", "emit_denial", "plugin",
262
+ __all__ = ["GROUP", "KINDS", "Event", "EventsPlugin", "emit", "emit_denial", "emit_periodic_cost", "plugin",
198
263
  "plugins_for"]
@@ -247,12 +247,12 @@ async def handoff(
247
247
  profile is used, else `weight` routes to an available profile, else the
248
248
  repo's default agent.
249
249
 
250
- weight: "light", "medium" or "heavy" (when you don't need a specific
250
+ weight: "light", "medium", "heavy" or "advanced" (when you don't need a specific
251
251
  profile): brindle picks an available profile for the tier and the reply says
252
252
  which and why. light = small, well-specified, mechanical (docs, renames,
253
- simple tests); medium = normal feature work, refactors, cross-cutting edits,
254
- or bugfixes; heavy = reserved for exceptionally complex algorithmic work,
255
- subtle bugs that failed earlier fix rounds, or deep reasoning.
253
+ simple tests); medium = normal feature work or bugfixes; heavy = system
254
+ architecture, broad refactoring; advanced = complex, long-running
255
+ system-design and implementation tasks.
256
256
 
257
257
  Waits up to wait_seconds (default 4 minutes). If the worker isn't done by
258
258
  then, this returns "still running": call wait_for_worker to keep waiting,
@@ -384,12 +384,12 @@ async def assign(
384
384
  profile is used, else `weight` routes to an available profile, else the
385
385
  repo's default agent.
386
386
 
387
- weight: "light", "medium" or "heavy" (when you don't need a specific
387
+ weight: "light", "medium", "heavy" or "advanced" (when you don't need a specific
388
388
  profile): brindle picks an available profile for the tier and the reply says
389
389
  which and why. light = small, well-specified, mechanical (docs, renames,
390
- simple tests); medium = normal feature work, refactors, cross-cutting edits,
391
- or bugfixes; heavy = reserved for exceptionally complex algorithmic work,
392
- subtle bugs that failed earlier fix rounds, or deep reasoning.
390
+ simple tests); medium = normal feature work or bugfixes; heavy = system
391
+ architecture, broad refactoring; advanced = complex, long-running
392
+ system-design and implementation tasks.
393
393
 
394
394
  When it finishes, its result arrives in your conversation as a message.
395
395
  Isolation works as for handoff. Use this to run several workers in parallel.