brindle 0.5.1__tar.gz → 0.5.2__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.
- {brindle-0.5.1 → brindle-0.5.2}/PKG-INFO +1 -1
- {brindle-0.5.1 → brindle-0.5.2}/pyproject.toml +1 -1
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/agents.py +5 -5
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/autopilot.py +4 -4
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/check_scope.py +10 -6
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/cli.py +18 -12
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/codex_hook.py +20 -2
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/company_login.py +3 -1
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/config.py +77 -14
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/api-schema.json +20 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/routes_actions.py +61 -14
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/routes_read.py +65 -18
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/db.py +5 -2
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/gates.py +47 -6
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/git.py +7 -2
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/keystore.py +24 -1
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/mcp_server.py +1 -1
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pipeline.py +9 -9
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/policy.py +7 -4
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/license.py +48 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/provider_homes.py +35 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/providers.py +25 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/providers_install.py +28 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/service/dto.py +3 -1
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/service/routing.py +6 -3
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/worker_models.py +30 -13
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/workspaces.py +19 -10
- {brindle-0.5.1 → brindle-0.5.2}/.gitignore +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/LICENSE +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/README.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/SCHEDULE-A +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/__init__.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/__main__.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/account.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/airgap.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/antigravity.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/assembler.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/assets/braid_dark.png +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/assets/braid_light.png +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/budget.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/__init__.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/backend-auditor.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/coordinator.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/developer-codex.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/developer-fable.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/developer-gemini.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/developer-heavy.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/developer-light.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/developer-local.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/developer-opencode.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/developer-sonnet.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/developer.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/qa.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/reviewer-local.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/reviewer-opencode.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/reviewer.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/subagent.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_agents/supervisor.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_rules/__init__.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_rules/security/backend.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/builtin_rules/tests/only.md +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/bus.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/ci_adapters.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/ci_client.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/ci_federation.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/ci_hosts.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/clone.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/cloud_credential.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/cloud_login.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/codemap.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/codemap_docs.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/company_identity.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/conflicts.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/coordinator.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/cost.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/cost_estimate.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/cull.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/__init__.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/app.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/auth.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/events.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/holds.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/lifecycle.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/peer.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/routes_org_permissions.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/schema.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/session_review.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/daemon/terminal.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/debuglog.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/demo.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/detect.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/doctor.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/entry.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/escapes.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/events.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/guardrails.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/history.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/identity_lock.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/inbox.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/kb.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/key_check.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/keys_cmds.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/knowledge.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/launch_health.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/learned_rules.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/learning.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/legacy.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/local_setup.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/login_pause.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/model_access.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/model_availability.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/native/__init__.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/native/catalog.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/native/client.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/native/loop.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/native/lsp.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/native/permissions.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/native/runner.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/native/serve.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/native/tools.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/opencode.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/org_permissions.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pane_auth.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/permission_asks.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/permission_grants.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/permission_groups.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/permission_routing.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/permissions.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/picker.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/plugins.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pool.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/preflight.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pricing.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/__init__.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/_files.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/account.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/audit_chain.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/audit_export.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/auth.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/brief_learning.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/cost_centers.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/credentials.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/features.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/keys.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/learning.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/loopback.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/managed_models.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/org_profiles.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/orgkey.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/rollout.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/route_health.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/settings_sync.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/status.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/team_events.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/pro/team_policy.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/procs.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/profiles.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/prompt_answer.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/quota.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/repo_cmds.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/repos.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/rewind.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/rule_checks.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/savings.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/scratch.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/secrets.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/self_serve.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/service/__init__.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/service/errors.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/service/inbox.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/service/permissions.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/service/sessions.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/services.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/sessions.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/sidebar_follow.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/sidebar_logo.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/signin_pause.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/status_cache.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/tasks.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/tmux.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/updates.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/upgrade.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/usage.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/view.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/wake.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/watch.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/watch_costs.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/web/__init__.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/web/chat.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/web/dashboard.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/web/diff.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/web/server.py +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/web/static/app.js +0 -0
- {brindle-0.5.1 → brindle-0.5.2}/src/brindle/web/static/index.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brindle
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
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
|
|
@@ -180,7 +180,7 @@ def worker_guidance(ws: Workspace, report_only: bool = False) -> str:
|
|
|
180
180
|
from brindle.config import load_repo_config
|
|
181
181
|
|
|
182
182
|
try:
|
|
183
|
-
checks = load_repo_config(ws.repo_root).checks
|
|
183
|
+
checks = load_repo_config(ws.repo_root, branch=ws.base_branch).checks
|
|
184
184
|
except ValueError:
|
|
185
185
|
checks = []
|
|
186
186
|
parts = (codemap.guidance(ws), "" if report_only else test_guidance(checks),
|
|
@@ -2318,7 +2318,7 @@ def warm_checks(ws: Workspace) -> None:
|
|
|
2318
2318
|
from brindle.providers import brindle_invocation
|
|
2319
2319
|
|
|
2320
2320
|
try:
|
|
2321
|
-
if not load_repo_config(ws.repo_root).checks or git.dirty_files(ws.path):
|
|
2321
|
+
if not load_repo_config(ws.repo_root, branch=ws.base_branch).checks or git.dirty_files(ws.path):
|
|
2322
2322
|
return
|
|
2323
2323
|
except (ValueError, git.GitError):
|
|
2324
2324
|
return
|
|
@@ -2906,7 +2906,7 @@ def start_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | Non
|
|
|
2906
2906
|
from brindle.providers import brindle_invocation
|
|
2907
2907
|
|
|
2908
2908
|
_review_worktree_present(ws)
|
|
2909
|
-
cfg = cfg or
|
|
2909
|
+
cfg = cfg or workspaces.config_for(db, ws)
|
|
2910
2910
|
if not cfg.checks:
|
|
2911
2911
|
return request_review(db, caller, ws, profile, focus, cfg, second=second, slot=slot)
|
|
2912
2912
|
_review_kill_switch(ws)
|
|
@@ -3026,7 +3026,7 @@ def review_after_checks(db: DB, caller_id: str | None, ws: Workspace, profile: s
|
|
|
3026
3026
|
from brindle import gates, pipeline
|
|
3027
3027
|
from brindle.config import load_repo_config
|
|
3028
3028
|
|
|
3029
|
-
cfg = cfg or
|
|
3029
|
+
cfg = cfg or workspaces.config_for(db, ws)
|
|
3030
3030
|
slot = _review_claim_slot(profile, slot)
|
|
3031
3031
|
profile = _review_profile(db, ws, profile, cfg)
|
|
3032
3032
|
with _review_request_run(ws.id, profile, request_id), pipeline.review_run(ws.id):
|
|
@@ -3166,7 +3166,7 @@ def _request_review(db: DB, caller: Agent | None, ws: Workspace, profile: str |
|
|
|
3166
3166
|
|
|
3167
3167
|
_review_worktree_present(ws)
|
|
3168
3168
|
_review_kill_switch(ws)
|
|
3169
|
-
cfg = cfg or
|
|
3169
|
+
cfg = cfg or workspaces.config_for(db, ws)
|
|
3170
3170
|
slot = _review_claim_slot(profile, slot)
|
|
3171
3171
|
profile = _review_profile(db, ws, profile, cfg)
|
|
3172
3172
|
if second and not slot:
|
|
@@ -47,7 +47,7 @@ import time
|
|
|
47
47
|
from dataclasses import dataclass
|
|
48
48
|
from pathlib import Path
|
|
49
49
|
|
|
50
|
-
from brindle.config import CONFIG_DIR, WEIGHTS, RepoConfig, config_root, load_repo_config
|
|
50
|
+
from brindle.config import CONFIG_DIR, WEIGHTS, RepoConfig, config_root, load_repo_config, workspace_config
|
|
51
51
|
from brindle import bus, escapes, knowledge
|
|
52
52
|
from brindle.db import DB, Agent, Autopilot, Milestone, Workspace, check_state
|
|
53
53
|
from brindle.profiles import load_profile
|
|
@@ -1310,7 +1310,7 @@ def check_milestones(db: DB, root_id: str, ws: Workspace, position: int | None =
|
|
|
1310
1310
|
ap = db.get_autopilot(root_id)
|
|
1311
1311
|
if ap and ap.state == "done":
|
|
1312
1312
|
return progress(db, root_id, in_flight=False)
|
|
1313
|
-
cfg = cfg or
|
|
1313
|
+
cfg = cfg or workspace_config(ws, session=True)
|
|
1314
1314
|
ms = db.milestones(root_id)
|
|
1315
1315
|
if not ms:
|
|
1316
1316
|
raise AutopilotError("no milestones yet: record the goal with set_goal first")
|
|
@@ -1363,7 +1363,7 @@ def check_milestones(db: DB, root_id: str, ws: Workspace, position: int | None =
|
|
|
1363
1363
|
# Cached by commit when the checkout is clean, so a check that
|
|
1364
1364
|
# already passed at this HEAD (as a merge gate, say) isn't re-run.
|
|
1365
1365
|
try:
|
|
1366
|
-
ok, out = gates.run_checked(db, checkout, m.check_cmd, workspaces.workspace_env(checkout),
|
|
1366
|
+
ok, out = gates.run_checked(db, checkout, m.check_cmd, workspaces.workspace_env(checkout, repo_cfg),
|
|
1367
1367
|
repo_cfg.check_timeout)
|
|
1368
1368
|
except Exception as e:
|
|
1369
1369
|
db.record_check(m.id, False, f"$ {m.check_cmd}\n(crashed: {e})", sha,
|
|
@@ -1569,7 +1569,7 @@ def start_audit(db: DB, root_id: str, ws: Workspace, cfg: RepoConfig | None = No
|
|
|
1569
1569
|
if ap.audit_sha == head:
|
|
1570
1570
|
return None
|
|
1571
1571
|
agents.close(db, running.id)
|
|
1572
|
-
cfg = cfg or
|
|
1572
|
+
cfg = cfg or workspace_config(ws, session=True)
|
|
1573
1573
|
profile = agents.default_review_profile(cfg, db.get_agent(root_id), db, ws.repo_root, ap.goal)
|
|
1574
1574
|
auditor = agents.spawn(db, ws, profile, prompt=audit_prompt(db, ap, head), parent_id=root_id,
|
|
1575
1575
|
mode="review", background_setup=True)
|
|
@@ -14,7 +14,9 @@ left its base decide what runs:
|
|
|
14
14
|
- one matches ``INFER_FULL_WHEN`` (conftest, pytest and project config, a
|
|
15
15
|
lockfile) or is under ``.brindle/``, or the scope can't be worked out: the
|
|
16
16
|
command runs in full;
|
|
17
|
-
- otherwise
|
|
17
|
+
- otherwise changed files that aren't ``.py`` are ignored (pytest can't be
|
|
18
|
+
affected by them), and if none is left the check is skipped; the command
|
|
19
|
+
runs on the test files the branch touched plus the
|
|
18
20
|
tests ``graphify affected`` reaches from each changed symbol. A changed file
|
|
19
21
|
the graph can't map to a test runs the full suite instead.
|
|
20
22
|
|
|
@@ -61,10 +63,9 @@ _VALUE_OPTIONS = {"-n", "-m", "-k", "-p", "-o", "-c", "--maxfail", "--tb", "--du
|
|
|
61
63
|
"--deselect", "--dist", "--cov", "--timeout", "--junitxml", "-W", "--basetemp"}
|
|
62
64
|
INFER_FULL_WHEN = ["**/conftest.py", "**/pyproject.toml", "**/setup.cfg", "**/pytest.ini", "**/tox.ini",
|
|
63
65
|
"**/*.lock", "**/*lock.json", "**/requirements*.txt"]
|
|
64
|
-
NOTICE = ("brindle
|
|
65
|
-
"using the code graph
|
|
66
|
-
'`"scoped_checks": "
|
|
67
|
-
'current behaviour with `"scoped_checks": "off"`.')
|
|
66
|
+
NOTICE = ("brindle now runs only the tests a worker branch's changes reach for a plain pytest check (`{cmd}`), "
|
|
67
|
+
"using the code graph. To always run it in full, set "
|
|
68
|
+
'`"scoped_checks": "off"` in .brindle/config.json (or ~/.brindle/config.json).')
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
def pytest_selectors(cmd: str) -> bool | None:
|
|
@@ -99,7 +100,7 @@ def inferred(cmd: str, mode: str | None) -> CheckScope | None:
|
|
|
99
100
|
"""The scope ``scoped_checks: "infer"`` gives ``cmd``: pytest on the
|
|
100
101
|
affected tests when it is a plain pytest command that picks no tests
|
|
101
102
|
itself (appending targets would not narrow it), else None."""
|
|
102
|
-
if mode
|
|
103
|
+
if mode in (None, "infer") and pytest_selectors(cmd) is False:
|
|
103
104
|
return CheckScope(INFER_FULL_WHEN)
|
|
104
105
|
return None
|
|
105
106
|
|
|
@@ -176,6 +177,9 @@ def command(ws: Workspace, cmd: str, scope: CheckScope | None, diff: tuple[str,
|
|
|
176
177
|
return None
|
|
177
178
|
if any(matches(f, ALWAYS_FULL) or matches(f, scope.full_when) for f in changed):
|
|
178
179
|
return cmd
|
|
180
|
+
changed = [f for f in changed if f.endswith(".py")] # pytest can't be affected by the rest
|
|
181
|
+
if not changed:
|
|
182
|
+
return None
|
|
179
183
|
tests = pytest_targets(ws, base, changed)
|
|
180
184
|
return f"{cmd} {_quoted(tests)} || [ $? -eq 5 ]" if tests else cmd # 5: none of them selected
|
|
181
185
|
|
|
@@ -2333,11 +2333,10 @@ def open_cmd(
|
|
|
2333
2333
|
@app.command()
|
|
2334
2334
|
def setup(workspace: Optional[str] = typer.Argument(None)) -> None:
|
|
2335
2335
|
"""Re-run setup commands in a workspace."""
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
res = workspaces.run_commands(cmds, ws.path, workspaces.workspace_env(ws))
|
|
2336
|
+
db = DB()
|
|
2337
|
+
ws = _ws(db, workspace)
|
|
2338
|
+
cfg = workspaces.config_for(db, ws)
|
|
2339
|
+
res = workspaces.run_commands(cfg.setup, ws.path, workspaces.workspace_env(ws, cfg))
|
|
2341
2340
|
typer.echo(res.log or "(no setup commands)")
|
|
2342
2341
|
raise typer.Exit(0 if res.ok else 1)
|
|
2343
2342
|
|
|
@@ -2407,8 +2406,15 @@ def pr(
|
|
|
2407
2406
|
draft: bool = typer.Option(False, "--draft"),
|
|
2408
2407
|
) -> None:
|
|
2409
2408
|
"""Push and open a pull request against the base branch."""
|
|
2410
|
-
|
|
2411
|
-
|
|
2409
|
+
from brindle import gates
|
|
2410
|
+
|
|
2411
|
+
db = DB()
|
|
2412
|
+
ws = _ws(db, workspace)
|
|
2413
|
+
def open_checked() -> str:
|
|
2414
|
+
with gates.pr_checked(db, ws, lambda text: typer.echo(text, err=True)) as sha:
|
|
2415
|
+
return workspaces.pull_request(ws, title, draft, sha=sha)
|
|
2416
|
+
|
|
2417
|
+
url = _run(open_checked)
|
|
2412
2418
|
typer.echo(url)
|
|
2413
2419
|
_offer_delete_on_merge(ws.repo_root)
|
|
2414
2420
|
|
|
@@ -2453,12 +2459,12 @@ def services_cmd(
|
|
|
2453
2459
|
) -> None:
|
|
2454
2460
|
"""Per-worktree Docker services (brindle Pro): list, start or stop a workspace's."""
|
|
2455
2461
|
from brindle import services as services_mod
|
|
2456
|
-
from brindle.config import load_repo_config
|
|
2457
2462
|
|
|
2458
2463
|
if action not in ("ls", "up", "down"):
|
|
2459
2464
|
_fail("action must be ls, up or down")
|
|
2460
|
-
|
|
2461
|
-
|
|
2465
|
+
db = DB()
|
|
2466
|
+
ws = _ws(db, workspace)
|
|
2467
|
+
cfg = workspaces.config_for(db, ws)
|
|
2462
2468
|
if not cfg.services:
|
|
2463
2469
|
_fail('no services configured; add a "services" list to .brindle/config.json')
|
|
2464
2470
|
if action == "up":
|
|
@@ -3100,7 +3106,7 @@ def review_after_checks_cmd(workspace_id: str, caller: Optional[str] = typer.Opt
|
|
|
3100
3106
|
if ws is None:
|
|
3101
3107
|
return
|
|
3102
3108
|
try:
|
|
3103
|
-
agents.review_after_checks(db, caller, ws, profile, focus,
|
|
3109
|
+
agents.review_after_checks(db, caller, ws, profile, focus, workspaces.config_for(db, ws),
|
|
3104
3110
|
request_id=request_id, second=second, slot=slot)
|
|
3105
3111
|
except (agents.AgentError, ValueError) as e:
|
|
3106
3112
|
from brindle import pipeline
|
|
@@ -3170,7 +3176,7 @@ def warm_checks_cmd(workspace_id: str) -> None:
|
|
|
3170
3176
|
try:
|
|
3171
3177
|
# A new commit makes this run's result useless (the review and the
|
|
3172
3178
|
# gate want the new head), so give up and free the check slot.
|
|
3173
|
-
gates.check_summary(db, ws,
|
|
3179
|
+
gates.check_summary(db, ws, workspaces.config_for(db, ws),
|
|
3174
3180
|
cancel=lambda: gates.head(ws) != sha)
|
|
3175
3181
|
except gates.Abandoned:
|
|
3176
3182
|
pass
|
|
@@ -73,6 +73,20 @@ def _home_key(home: Path) -> str:
|
|
|
73
73
|
return os.path.realpath(home)
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def _runnable_script(script: str) -> bool:
|
|
77
|
+
if not os.access(script, os.X_OK):
|
|
78
|
+
return False
|
|
79
|
+
try:
|
|
80
|
+
with open(script, "rb") as f:
|
|
81
|
+
first = f.readline(4096)
|
|
82
|
+
except OSError:
|
|
83
|
+
return False
|
|
84
|
+
if first.startswith(b"#!"):
|
|
85
|
+
interpreter = first[2:].split(maxsplit=1)
|
|
86
|
+
return bool(interpreter) and os.access(os.fsdecode(interpreter[0]), os.X_OK)
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
76
90
|
def _stable_invocation() -> list[str]:
|
|
77
91
|
"""brindle_invocation, with the interpreter named the same however brindle
|
|
78
92
|
was started (a venv's ``python`` and ``python3`` are the same program,
|
|
@@ -87,8 +101,12 @@ def _stable_invocation() -> list[str]:
|
|
|
87
101
|
# replaces the other's and the next launch of either stops at "Hooks need review".
|
|
88
102
|
script = os.path.join(os.path.dirname(exe), "brindle")
|
|
89
103
|
if len(argv) == 1:
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
if _runnable_script(exe):
|
|
105
|
+
return [os.path.realpath(exe)]
|
|
106
|
+
# Older bundled engines carry a console script with a CI-only shebang.
|
|
107
|
+
exe = os.path.join(os.path.dirname(os.path.realpath(exe)), "python3")
|
|
108
|
+
argv = [exe, "-P", "-m", "brindle"]
|
|
109
|
+
if argv[1:] == ["-P", "-m", "brindle"] and _runnable_script(script):
|
|
92
110
|
return [os.path.realpath(script)]
|
|
93
111
|
plain = os.path.join(os.path.dirname(exe), "python")
|
|
94
112
|
try:
|
|
@@ -24,6 +24,7 @@ from pathlib import Path
|
|
|
24
24
|
from typing import Callable
|
|
25
25
|
|
|
26
26
|
from brindle import cloud_credential, config
|
|
27
|
+
from brindle.pro.license import request_cached
|
|
27
28
|
|
|
28
29
|
log = logging.getLogger(__name__)
|
|
29
30
|
|
|
@@ -575,6 +576,7 @@ def route_in_use(org_id: str, setup) -> bool:
|
|
|
575
576
|
return isinstance(got, dict) and got.get("org") == org_id
|
|
576
577
|
|
|
577
578
|
|
|
579
|
+
@request_cached
|
|
578
580
|
def org_setup(repo_root: str | None = None, store=None, client=None, max_age: float | None = None):
|
|
579
581
|
"""(org id, AgentSetup) for the signed-in org; personal policies supply
|
|
580
582
|
only federated Claude setup. None when there is no applicable setup;
|
|
@@ -583,8 +585,8 @@ def org_setup(repo_root: str | None = None, store=None, client=None, max_age: fl
|
|
|
583
585
|
from brindle.pro import credentials, license, team_policy
|
|
584
586
|
|
|
585
587
|
try:
|
|
586
|
-
store = store or credentials.default_store()
|
|
587
588
|
ent = license.current(store=store, client=client, max_age=max_age)
|
|
589
|
+
store = store or credentials.default_store()
|
|
588
590
|
personal = team_policy.FEATURE not in ent.features
|
|
589
591
|
if personal and (ent.policy_version <= 0 or (store.load() or {}).get("org_id")
|
|
590
592
|
or ent.plan in ("team", "enterprise")):
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import json
|
|
12
12
|
import os
|
|
13
|
+
import subprocess
|
|
13
14
|
import sys
|
|
14
15
|
import tempfile
|
|
15
16
|
import threading
|
|
@@ -106,7 +107,7 @@ class RepoConfig:
|
|
|
106
107
|
# Autopilot and merge gates.
|
|
107
108
|
autopilot: bool = True # `brindle` starts the supervisor with autopilot on
|
|
108
109
|
checks: list[str] = field(default_factory=list) # must pass in a branch before it merges
|
|
109
|
-
scoped_checks: str | None = None # "off" or "infer"; None: not chosen (acts as "
|
|
110
|
+
scoped_checks: str | None = None # "off" or "infer"; None: not chosen (acts as "infer", see brindle.check_scope.inferred)
|
|
110
111
|
review: bool | None = None # require a reviewer's approval (None: only under autopilot)
|
|
111
112
|
reviewer: str = "reviewer" # agent profile that reviews branches
|
|
112
113
|
review_profile: str | None = None # force request_review's profile (skips its automatic cross-model pick)
|
|
@@ -244,11 +245,11 @@ def _merge_commands(shared: list[str], local: object) -> list[str]:
|
|
|
244
245
|
return shared
|
|
245
246
|
|
|
246
247
|
|
|
247
|
-
def _read_json(path: Path) -> dict:
|
|
248
|
-
if not path.is_file():
|
|
248
|
+
def _read_json(path: Path, text: str | None = None) -> dict:
|
|
249
|
+
if text is None and not path.is_file():
|
|
249
250
|
return {}
|
|
250
251
|
try:
|
|
251
|
-
data = json.loads(path.read_text(encoding="utf-8"))
|
|
252
|
+
data = json.loads(path.read_text(encoding="utf-8") if text is None else text)
|
|
252
253
|
except json.JSONDecodeError as e:
|
|
253
254
|
raise ValueError(f"{path}: invalid JSON ({e})") from e
|
|
254
255
|
if not isinstance(data, dict):
|
|
@@ -256,6 +257,30 @@ def _read_json(path: Path) -> dict:
|
|
|
256
257
|
return data
|
|
257
258
|
|
|
258
259
|
|
|
260
|
+
def _read_shared(root: Path, branch: str | None) -> dict:
|
|
261
|
+
"""The shared config as committed on ``branch`` (the branch a session's
|
|
262
|
+
workers are cut from and merge into), not whatever the checkout at
|
|
263
|
+
``root`` has checked out; the working tree's file when there's no
|
|
264
|
+
branch or it isn't committed there."""
|
|
265
|
+
path = root / CONFIG_DIR / CONFIG_FILE
|
|
266
|
+
if branch:
|
|
267
|
+
try:
|
|
268
|
+
proc = subprocess.run(["git", "show", f"{branch}:{CONFIG_DIR}/{CONFIG_FILE}"], cwd=root,
|
|
269
|
+
capture_output=True, text=True)
|
|
270
|
+
except OSError:
|
|
271
|
+
proc = None
|
|
272
|
+
if proc is not None and proc.returncode == 0:
|
|
273
|
+
return _read_json(Path(f"{branch}:{CONFIG_DIR}/{CONFIG_FILE}"), proc.stdout)
|
|
274
|
+
return _read_json(path)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def workspace_config(ws, *, session: bool = False) -> RepoConfig:
|
|
278
|
+
"""``load_repo_config`` for workspace ``ws``, reading the shared file
|
|
279
|
+
from its base branch (see ``_read_shared``). A session's own workspace
|
|
280
|
+
(``session``) reads its own branch; its workers' base is that branch."""
|
|
281
|
+
return load_repo_config(ws.repo_root, branch=ws.branch if session and ws.kind == "worktree" else ws.base_branch)
|
|
282
|
+
|
|
283
|
+
|
|
259
284
|
def config_root(path: str | Path) -> Path:
|
|
260
285
|
"""The directory whose ``.brindle`` applies to ``path``: ``path`` itself when
|
|
261
286
|
it has one, else (for a linked git worktree, where the git-ignored config
|
|
@@ -342,10 +367,14 @@ def kind_overrides(found: object) -> dict[str, dict[str, str]]:
|
|
|
342
367
|
return out
|
|
343
368
|
|
|
344
369
|
|
|
345
|
-
def load_repo_config(repo_root: str | Path) -> RepoConfig:
|
|
370
|
+
def load_repo_config(repo_root: str | Path, *, user_only: bool = False, branch: str | None = None) -> RepoConfig:
|
|
371
|
+
"""The settings for ``repo_root``: its local file over its shared one over
|
|
372
|
+
the person's ~/.brindle/config.json. ``user_only``: the person's file
|
|
373
|
+
alone, as every repo starts from it (the app's Agents tab). ``branch``:
|
|
374
|
+
read the shared file as committed there (see ``_read_shared``)."""
|
|
346
375
|
base = config_root(repo_root) / CONFIG_DIR
|
|
347
|
-
shared =
|
|
348
|
-
local = _read_json(base / LOCAL_CONFIG_FILE)
|
|
376
|
+
shared = {} if user_only else _read_shared(base.parent, branch)
|
|
377
|
+
local = {} if user_only else _read_json(base / LOCAL_CONFIG_FILE)
|
|
349
378
|
user = user_settings()
|
|
350
379
|
|
|
351
380
|
cfg = RepoConfig()
|
|
@@ -402,7 +431,8 @@ def load_repo_config(repo_root: str | Path) -> RepoConfig:
|
|
|
402
431
|
cfg.routing_overrides.setdefault(kind, {}).update(fields)
|
|
403
432
|
from brindle import worker_models
|
|
404
433
|
|
|
405
|
-
|
|
434
|
+
# The models picked on the picker's workers screen (the person's own profiles, for user_only).
|
|
435
|
+
worker_models.apply(cfg, user, str(repo_root), user_library=user_only)
|
|
406
436
|
for source in (shared, local): # per weight, so a repo can override one and keep the rest
|
|
407
437
|
second = source.get("second_review")
|
|
408
438
|
if isinstance(second, dict):
|
|
@@ -520,15 +550,33 @@ def _sync_setting(key: str) -> None:
|
|
|
520
550
|
pass
|
|
521
551
|
|
|
522
552
|
|
|
553
|
+
_REPO_WRITE = threading.Lock()
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _change_json(path: Path, change) -> None:
|
|
557
|
+
"""Read ``path``, apply ``change`` to its dict and replace the file, one at a
|
|
558
|
+
time (daemon actions run in threads) through a unique temp file in the same
|
|
559
|
+
directory, so a reader never sees half a file and no write is lost."""
|
|
560
|
+
with _REPO_WRITE:
|
|
561
|
+
data = _read_json(path)
|
|
562
|
+
change(data)
|
|
563
|
+
fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=path.name + ".", suffix=".tmp")
|
|
564
|
+
try:
|
|
565
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
566
|
+
f.write(json.dumps(data, indent=2) + "\n")
|
|
567
|
+
os.replace(tmp, path)
|
|
568
|
+
except BaseException:
|
|
569
|
+
Path(tmp).unlink(missing_ok=True)
|
|
570
|
+
raise
|
|
571
|
+
|
|
572
|
+
|
|
523
573
|
def set_local(repo_root: str | Path, key: str, value: object) -> Path:
|
|
524
574
|
"""Set one key in ``.brindle/config.local.json`` (gitignored: this person's
|
|
525
575
|
own settings; other keys are kept)."""
|
|
526
576
|
base = config_root(repo_root) / CONFIG_DIR
|
|
527
577
|
base.mkdir(parents=True, exist_ok=True)
|
|
528
578
|
path = base / LOCAL_CONFIG_FILE
|
|
529
|
-
data
|
|
530
|
-
data[key] = value
|
|
531
|
-
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
579
|
+
_change_json(path, lambda data: data.__setitem__(key, value))
|
|
532
580
|
ignore = base / ".gitignore"
|
|
533
581
|
if not ignore.exists():
|
|
534
582
|
ignore.write_text(f"{LOCAL_CONFIG_FILE}\n", encoding="utf-8")
|
|
@@ -540,9 +588,24 @@ def set_repo(repo_root: str | Path, key: str, value: object) -> Path:
|
|
|
540
588
|
everyone's settings; other keys are kept)."""
|
|
541
589
|
path = scope_paths(repo_root)["repo"]
|
|
542
590
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
543
|
-
data
|
|
544
|
-
|
|
545
|
-
|
|
591
|
+
_change_json(path, lambda data: data.__setitem__(key, value))
|
|
592
|
+
return path
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def unset_local(repo_root: str | Path, key: str) -> Path:
|
|
596
|
+
"""Remove one key from ``.brindle/config.local.json`` (other keys are kept),
|
|
597
|
+
so the repo's shared file or the person's own value reads through again."""
|
|
598
|
+
path = scope_paths(repo_root)["local"]
|
|
599
|
+
if path.exists():
|
|
600
|
+
_change_json(path, lambda data: data.pop(key, None))
|
|
601
|
+
return path
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def unset_repo(repo_root: str | Path, key: str) -> Path:
|
|
605
|
+
"""Remove one key from the repo's committed ``.brindle/config.json``."""
|
|
606
|
+
path = scope_paths(repo_root)["repo"]
|
|
607
|
+
if path.exists():
|
|
608
|
+
_change_json(path, lambda data: data.pop(key, None))
|
|
546
609
|
return path
|
|
547
610
|
|
|
548
611
|
|
|
@@ -5487,6 +5487,18 @@
|
|
|
5487
5487
|
"title": "Mode",
|
|
5488
5488
|
"type": "string"
|
|
5489
5489
|
},
|
|
5490
|
+
"name": {
|
|
5491
|
+
"anyOf": [
|
|
5492
|
+
{
|
|
5493
|
+
"type": "string"
|
|
5494
|
+
},
|
|
5495
|
+
{
|
|
5496
|
+
"type": "null"
|
|
5497
|
+
}
|
|
5498
|
+
],
|
|
5499
|
+
"default": null,
|
|
5500
|
+
"title": "Name"
|
|
5501
|
+
},
|
|
5490
5502
|
"profile": {
|
|
5491
5503
|
"title": "Profile",
|
|
5492
5504
|
"type": "string"
|
|
@@ -6136,6 +6148,10 @@
|
|
|
6136
6148
|
"method": "POST",
|
|
6137
6149
|
"path": "/v1/providers/{provider}/login"
|
|
6138
6150
|
},
|
|
6151
|
+
{
|
|
6152
|
+
"method": "POST",
|
|
6153
|
+
"path": "/v1/providers/{provider}/logout"
|
|
6154
|
+
},
|
|
6139
6155
|
{
|
|
6140
6156
|
"method": "GET",
|
|
6141
6157
|
"path": "/v1/providers/{provider}/preflight"
|
|
@@ -6228,6 +6244,10 @@
|
|
|
6228
6244
|
"method": "POST",
|
|
6229
6245
|
"path": "/v1/sessions/{root}/pause"
|
|
6230
6246
|
},
|
|
6247
|
+
{
|
|
6248
|
+
"method": "POST",
|
|
6249
|
+
"path": "/v1/sessions/{root}/rename"
|
|
6250
|
+
},
|
|
6231
6251
|
{
|
|
6232
6252
|
"method": "POST",
|
|
6233
6253
|
"path": "/v1/sessions/{root}/transfer"
|
|
@@ -36,7 +36,7 @@ from starlette.requests import Request
|
|
|
36
36
|
from starlette.responses import JSONResponse
|
|
37
37
|
from starlette.routing import Route
|
|
38
38
|
|
|
39
|
-
from brindle import agents, autopilot, bus, git, identity_lock, knowledge, pipeline, preflight, scratch, tmux, workspaces
|
|
39
|
+
from brindle import agents, autopilot, bus, gates, git, identity_lock, knowledge, pipeline, preflight, scratch, tmux, workspaces
|
|
40
40
|
from brindle import sessions as sessions_mod
|
|
41
41
|
from brindle.daemon import session_review as review
|
|
42
42
|
from brindle.daemon import terminal
|
|
@@ -227,6 +227,7 @@ def start_session(db: DB, params: dict, body: Body) -> Any:
|
|
|
227
227
|
sign-in or first-run screen (see brindle.preflight). Only opening a
|
|
228
228
|
session that is running now launches nothing, so only that isn't checked."""
|
|
229
229
|
repo, choice, session_id = _get(body, "repo", required=True), _get(body, "choice"), _get(body, "session_id")
|
|
230
|
+
name = _session_name(_get(body, "name")) # checked with the rest, before anything launches
|
|
230
231
|
opening = (choice == "o" and not _get(body, "branch") and not _get(body, "worktree")
|
|
231
232
|
and any(s.root.id == session_id
|
|
232
233
|
for s in sessions_mod.live(db, workspaces.adopt_root(db, repo).repo_root)))
|
|
@@ -242,6 +243,9 @@ def start_session(db: DB, params: dict, body: Body) -> Any:
|
|
|
242
243
|
if isinstance(got, svc.Opened):
|
|
243
244
|
return {"opened": SessionDTO.from_agent(got.root).to_json()}
|
|
244
245
|
if isinstance(got, svc.Started):
|
|
246
|
+
got.agent.name = name
|
|
247
|
+
if name:
|
|
248
|
+
db.update_agent(got.agent.id, name=name)
|
|
245
249
|
return {**got.to_json(), "notes": _notes(got.notes)}
|
|
246
250
|
return got
|
|
247
251
|
|
|
@@ -267,6 +271,19 @@ def close_session(db: DB, params: dict, body: Body) -> Any:
|
|
|
267
271
|
return {"closed": [a.id for a, _ in closed], "stopped": [a.id for a, was in closed if was]}
|
|
268
272
|
|
|
269
273
|
|
|
274
|
+
def _session_name(raw: str | None) -> str | None:
|
|
275
|
+
"""A session's name as kept: whitespace collapsed, at most 80 characters, None when empty."""
|
|
276
|
+
return " ".join((raw or "").split())[:80] or None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def rename_session(db: DB, params: dict, body: Body) -> Any:
|
|
280
|
+
"""``name``: the session's new name; empty clears it (listed by its goal again)."""
|
|
281
|
+
root, _ = _root(db, params["root"])
|
|
282
|
+
name = _session_name(_get(body, "name"))
|
|
283
|
+
db.update_agent(root.id, name=name)
|
|
284
|
+
return {"id": root.id, "name": name}
|
|
285
|
+
|
|
286
|
+
|
|
270
287
|
def handover_session(db: DB, params: dict, body: Body) -> Any:
|
|
271
288
|
root, ws = _root(db, params["root"])
|
|
272
289
|
dest = workspaces.checkout_for_target(db, ws.path, _get(body, "to", required=True))
|
|
@@ -414,7 +431,7 @@ def review_workspace(db: DB, params: dict, body: Body) -> Any:
|
|
|
414
431
|
raise ServiceError("invalid", "only a worker's workspace (its own branch and worktree) can be reviewed")
|
|
415
432
|
# With checks, a detached helper runs them first; review.submitted ends it.
|
|
416
433
|
reviewer = agents.start_review(db, None, ws, _get(body, "profile"), _get(body, "focus"),
|
|
417
|
-
load_repo_config(ws.repo_root))
|
|
434
|
+
load_repo_config(ws.repo_root, branch=ws.base_branch))
|
|
418
435
|
return {"reviewer": reviewer.id if reviewer else None, "checks_running": reviewer is None}
|
|
419
436
|
|
|
420
437
|
|
|
@@ -741,8 +758,8 @@ def open_session_pr(db: DB, params: dict, body: Body, auto: bool = False) -> Any
|
|
|
741
758
|
def work(db: DB, job: Job) -> Any:
|
|
742
759
|
try:
|
|
743
760
|
identity_lock.ensure(root) # again: the person signed in may have changed since
|
|
744
|
-
|
|
745
|
-
|
|
761
|
+
with gates.pr_checked(db, ws, lambda t: bus.publish(db, "job.progress", job=job.id, text=t)) as head:
|
|
762
|
+
url = workspaces.open_pull_request(ws, title, draft, body=text, sha=head)
|
|
746
763
|
if url is None and auto:
|
|
747
764
|
raise workspaces.WorkspaceError("pushed, but gh couldn't open the PR")
|
|
748
765
|
if url is None: # the person opens it from the compare page: no PR to record yet
|
|
@@ -935,17 +952,26 @@ def _fits(value: Any, default: Any) -> bool:
|
|
|
935
952
|
def patch_config(db: DB, params: dict, body: Body) -> Any:
|
|
936
953
|
"""``values`` into ~/.brindle/config.json (scope "user") or the repo's
|
|
937
954
|
.brindle/config.local.json (scope "repo"). Only the repo config's own
|
|
938
|
-
keys, each with a value of its default's type (or null).""
|
|
955
|
+
keys, each with a value of its default's type (or null). Scope "shared"
|
|
956
|
+
is the repo's committed .brindle/config.json. ``unset`` (scope "repo" or
|
|
957
|
+
"shared") lists keys to remove from that file, clearing a project's
|
|
958
|
+
override."""
|
|
939
959
|
from brindle import config
|
|
940
960
|
|
|
941
961
|
scope = _get(body, "scope", default="user")
|
|
942
|
-
if scope not in ("user", "repo"):
|
|
943
|
-
raise ServiceError("invalid", "scope must be user or
|
|
944
|
-
repo = _get(body, "repo", required=scope
|
|
945
|
-
values = _get(body, "values", dict,
|
|
962
|
+
if scope not in ("user", "repo", "shared"):
|
|
963
|
+
raise ServiceError("invalid", "scope must be user, repo or shared")
|
|
964
|
+
repo = _get(body, "repo", required=scope != "user")
|
|
965
|
+
values = _get(body, "values", dict, default={})
|
|
966
|
+
unset = _get(body, "unset", list, default=[])
|
|
967
|
+
if unset and scope == "user":
|
|
968
|
+
raise ServiceError("invalid", "unset needs scope repo or shared")
|
|
969
|
+
if not values and not unset:
|
|
970
|
+
raise ServiceError("invalid", "values is required")
|
|
971
|
+
path = None
|
|
946
972
|
defaults = config.RepoConfig()
|
|
947
973
|
known = {f.name for f in dataclasses.fields(config.RepoConfig)}
|
|
948
|
-
if unknown := sorted(set(values) - known):
|
|
974
|
+
if unknown := sorted((set(values) | set(unset)) - known):
|
|
949
975
|
raise ServiceError("invalid", f"unknown setting(s): {', '.join(unknown)}")
|
|
950
976
|
for key, value in values.items():
|
|
951
977
|
if not _fits(value, getattr(defaults, key)):
|
|
@@ -953,9 +979,13 @@ def patch_config(db: DB, params: dict, body: Body) -> Any:
|
|
|
953
979
|
for key, value in values.items():
|
|
954
980
|
if scope == "user":
|
|
955
981
|
path = config.set_user(key, value)
|
|
982
|
+
elif scope == "shared":
|
|
983
|
+
path = config.set_repo(repo, key, value)
|
|
956
984
|
else:
|
|
957
985
|
path = config.set_local(repo, key, value)
|
|
958
|
-
|
|
986
|
+
for key in unset:
|
|
987
|
+
path = config.unset_repo(repo, key) if scope == "shared" else config.unset_local(repo, key)
|
|
988
|
+
return {"updated": sorted({*values, *unset}), "path": str(path)}
|
|
959
989
|
|
|
960
990
|
|
|
961
991
|
# -- knowledge sources (brindle kb add / sync / remove) -----------------------------
|
|
@@ -1093,7 +1123,8 @@ def put_routing(db: DB, params: dict, body: Body) -> Any:
|
|
|
1093
1123
|
lock_changes: dict[str, bool] = {}
|
|
1094
1124
|
overrides: dict[str, dict[str, str | None]] = {} # kind -> effort / instructions (None clears)
|
|
1095
1125
|
extra: dict[str, Any] = {} # supervisor and reviewer settings the kinds ask for
|
|
1096
|
-
|
|
1126
|
+
# A user-scope change builds on the person's own lists and library, never this repo's.
|
|
1127
|
+
current = config.load_repo_config(repo, user_only=scope == "user").routing
|
|
1097
1128
|
for kind, spec in kinds.items():
|
|
1098
1129
|
if kind not in config.ROUTING_KINDS or not isinstance(spec, dict):
|
|
1099
1130
|
raise ServiceError("invalid", f"kind must be one of {', '.join(config.ROUTING_KINDS)}, not {kind!r}")
|
|
@@ -1131,7 +1162,7 @@ def put_routing(db: DB, params: dict, body: Body) -> Any:
|
|
|
1131
1162
|
extra["supervisor_profile"] = model
|
|
1132
1163
|
if fallbacks is not None:
|
|
1133
1164
|
extra["supervisor_fallbacks"] = list(dict.fromkeys(fallbacks))
|
|
1134
|
-
by_name = {p.name: p for p in profiles.list_profiles(repo)}
|
|
1165
|
+
by_name = {p.name: p for p in profiles.list_profiles(None if scope == "user" else repo)}
|
|
1135
1166
|
known = set(by_name)
|
|
1136
1167
|
named = [reviewer] if reviewer is not None else []
|
|
1137
1168
|
for value in extra.values(): # supervisor and reviewer choices and fallbacks are profiles too
|
|
@@ -1146,7 +1177,7 @@ def put_routing(db: DB, params: dict, body: Body) -> Any:
|
|
|
1146
1177
|
raise ServiceError("invalid", f"no such profile(s): {', '.join(unknown)}")
|
|
1147
1178
|
if excluded is not None and (bad := sorted(set(map(str, excluded)) - set(providers.PROVIDERS))):
|
|
1148
1179
|
raise ServiceError("invalid", f"no such provider(s): {', '.join(bad)}")
|
|
1149
|
-
policy = RoutingPolicy.load(repo)
|
|
1180
|
+
policy = RoutingPolicy.load(repo, user_library=scope == "user")
|
|
1150
1181
|
if policy.applies and policy.access.error:
|
|
1151
1182
|
raise ServiceError("forbidden", f"routing can't be changed now: {policy.access.error}", 403)
|
|
1152
1183
|
for name in dict.fromkeys(named):
|
|
@@ -1694,6 +1725,20 @@ def turn_off_provider(db: DB, params: dict, body: Body) -> Any:
|
|
|
1694
1725
|
return providers_install.turn_off(_known_provider(params))
|
|
1695
1726
|
|
|
1696
1727
|
|
|
1728
|
+
def provider_logout(db: DB, params: dict, body: Body) -> Any:
|
|
1729
|
+
"""Sign brindle's own copy of the provider out (Claude Code, Codex only);
|
|
1730
|
+
the person's own CLI login is not touched. Answers the provider's row."""
|
|
1731
|
+
from brindle import providers_install
|
|
1732
|
+
|
|
1733
|
+
provider = _known_provider(params)
|
|
1734
|
+
if provider not in providers_install.SIGN_OUT:
|
|
1735
|
+
raise ServiceError("invalid", f"sign out is offered for {', '.join(providers_install.SIGN_OUT)}")
|
|
1736
|
+
try:
|
|
1737
|
+
return providers_install.sign_out(provider)
|
|
1738
|
+
except providers_install.InstallError as e:
|
|
1739
|
+
raise ServiceError("logout_failed", str(e), 422) from None
|
|
1740
|
+
|
|
1741
|
+
|
|
1697
1742
|
def terminal_input(db: DB, params: dict, body: Body) -> Any:
|
|
1698
1743
|
"""Type ``text``, one line the person typed or pasted in the app (a
|
|
1699
1744
|
sign-in's code), into the action terminal ``id``, then Enter unless
|
|
@@ -1922,6 +1967,7 @@ ROUTES = [
|
|
|
1922
1967
|
_post("/sessions/{root}/continue", continue_session),
|
|
1923
1968
|
_post("/sessions/{root}/pause", pause_session),
|
|
1924
1969
|
_post("/sessions/{root}/close", close_session),
|
|
1970
|
+
_post("/sessions/{root}/rename", rename_session),
|
|
1925
1971
|
_post("/sessions/{root}/handover", handover_session),
|
|
1926
1972
|
_post("/sessions/{root}/transfer", transfer_session),
|
|
1927
1973
|
# GET on the file path reads (routes_read); agent callers are refused by the guard.
|
|
@@ -1977,6 +2023,7 @@ ROUTES = [
|
|
|
1977
2023
|
# Like every route, refused to agent callers by the guard.
|
|
1978
2024
|
_post("/providers/{provider}/login", provider_login),
|
|
1979
2025
|
_post("/providers/{provider}/preflight", profile_preflight),
|
|
2026
|
+
_post("/providers/{provider}/logout", provider_logout),
|
|
1980
2027
|
_post("/providers/{provider}/trust", provider_trust),
|
|
1981
2028
|
_post("/providers/{provider}/consent", provider_consent),
|
|
1982
2029
|
_post("/providers/{provider}/install", install_provider),
|