Add a per-thread advisor switch - #5
Open
ChrBoebel wants to merge 2 commits into
Open
Conversation
`experimental_statusLabels` was folded into `presentation` (labels) in SDK 0.4.16. bb validates registerTool options in the host, not in the SDK bundled into dist/, so the published artifact is rejected at load time on a current bb regardless of which SDK it was built against: plugin advisor failed to load: registerTool: "experimental_statusLabels" was folded into "presentation" (labels) in SDK 0.4.16 (tool "advisor_review") Observed on bb 0.41.0, 466 ms after installing this commit from git:, followed by three further load failures. The option is spread from a constant rather than inlined because the vendored 0.4.2 declarations in types/ do not describe `presentation` yet, so an inline literal trips TypeScript's excess-property check.
The global Advisor setting is all-or-nothing: a thread where the reviewer is noise cannot opt out without turning it off everywhere. This adds a switch beside the harness selector in the composer, for a thread and for a new thread. A thread override wins over the global setting in both directions, so the global value is a default rather than a ceiling. Off means the advisor never acts on its own in that thread; anything explicitly asked for — "Review now", waiting for completion — still runs. Two tables, both appended to the migration list: advisor_thread_settings one row per overridden thread advisor_new_thread_default the choice armed in the new-thread composer The new-thread composer has no thread id to write against, and plugin settings are read-only from the plugin, so the choice is stored and materialized in thread.created. It is single-use: the next user-created thread consumes and clears it. An earlier sticky variant behaved as a second hidden global that shadowed the real setting with no way back. The gate is checked at every entry point that can start a review on the advisor's own initiative, including inside the tool's execute(): bb does not hot-mutate a running provider session's tool set, so advisor_review can still be present after the switch is flipped off, and it must refuse rather than review. It returns an explicit non-approval so the refusal is not read as a pass. Adds 18 tests (101 total), covering both override directions, single-use arming, other plugins' worker threads not consuming the arm, and the refusal in an already-running session.
This was referenced Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The global Advisor setting is all-or-nothing. A thread where the reviewer is noise — a scratch thread, a long debugging session, a thread that is itself working on Advisor — cannot opt out without switching the reviewer off everywhere. This adds a switch beside the harness selector in the composer.
Behaviour
A thread override wins over the global setting in both directions, so the global value is a default rather than a ceiling: on in a thread while the default is off works, and off in a thread while the default is on works.
The governing rule for "off" is that the advisor never acts on its own initiative in that thread. Anything explicitly asked for still happens — "Review now" and waiting for completion stay ungated. Only the automatic paths are gated.
The switch also appears in the new-thread composer, so a thread can start with the reviewer already off.
Storage
Two tables, both appended to the existing migration list rather than edited into it:
retireThread's permanent branch deletes the override with the rest of the thread's rows.The new-thread case
The new-thread composer has no thread id to write against, and
bb.settingsis read-only from inside the plugin (get()andonChange(), no setter). So the choice is stored plugin-side and materialized inthread.created.It is deliberately single-use: the next user-created thread consumes it and clears it.
thread.createdalso ignores threads with anoriginPluginId, so another plugin's worker thread cannot eat the arm. Resolving it lazily in the gate instead would have applied it to pre-existing threads, and an earlier sticky variant behaved as a second hidden global that shadowed the real setting with no way back.The gate inside
execute()Worth calling out, because it is the non-obvious one.
bb.agents.configureruns atthread.start/turn.submit, and a running provider session's tool set is not hot-mutated — soadvisor_reviewcan still be present after the switch is flipped off mid-session. The tool therefore re-checks the gate inexecute()and refuses.The refusal is explicit that it is not an approval, so the agent cannot read a skipped review as a pass:
I reproduced this end to end: with the switch on, the agent called the tool and got a real verdict; after switching the same thread off, the live session still had the tool, called it again, and got the refusal — with the review count unchanged.
CLI
bb advisor statusgainedAdvisor default:,New threads:andThread advisor:lines.Verification
npm run typecheckclean ·npm test101/101 (83 existing + 18 new) ·npm run buildsucceeds.The new tests cover both override directions, single-use arming, another plugin's worker thread not consuming the arm, and the
execute()refusal in an already-running session. I also checked three of them by mutation — breaking the guard order inthread.created, and the gate direction — and confirmed the intended test fails before reverting.dist/left untouched, same reasoning as #4.